yangsirgo / softwareTest

软件设计师考试
6 stars 2 forks source link

面向对象程序设计 java 题 #56

Open yangsirgo opened 3 years ago

yangsirgo commented 3 years ago

面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。 现实世界中,我们定义了“人”这种抽象概念,而具体的人则是“小明”、“小红”、“小军”等一个个具体的人。所以,“人”可以定义为一个类(class),而具体的人则是实例(instance)

  1. class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型: moxing

    class [类1] {   
     [属性1];
     // [类1] (){} //默认无参构造函数
     [类1] (属性1, 形参1){
        this.属性1 = 形参1; // 一般这里的形参命名与属性名一致
     }
    }
    
    // 代码示例
    class Person {
    public String name;
    public int age;
    }
  2. instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同: shili

[类名][对象名] = new [类名][参数列表]

 // 代码示例
Person ming = new Person();
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name

Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;
  1. 方法:

定义方法:

[ 访问控制符 ][返回类型][方法名]( 形参1类型 形参名,形参2 类型 形参名, ...... ){
    若干方法语句;
    return 方法返回值;
}
//访问控制符为 private/pubic/default/protected/abstract

//代码实现
class Person {
    private String name;
    private int birth;

    public void setBirth(int birth) {
        this.birth = birth;
    }

    public int getAge() {
        return calcAge(2019); // 调用private方法
    }

    // private方法:
    private int calcAge(int currentYear) {
        return currentYear - this.birth;
    }
}

// 方法返回值通过return语句实现,如果没有返回值,返回类型设置为void,可以省略return。
// 定义private方法的理由是内部方法是可以调用private方法的.
  1. 构造方法: 创建实例的时候,我们经常需要同时初始化这个实例的字段,例如:
    
    Person ming = new Person();
    ming.setName("小明");
    ming.setAge(12);
初始化对象实例需要3行代码,而且,如果忘了调用setName()或者setAge(),这个实例内部的状态就是不正确的。

能否在创建对象实例时就把内部字段全部初始化为合适的值?

完全可以。

这时,我们就需要构造方法。

创建实例的时候,实际上是通过构造方法来初始化实例的。我们先来定义一个构造方法,能在创建Person实例的时候,一次性传入name和age,完成初始化:

class [类1] {
[属性1]; // [类1] (){} //默认无参构造函数 [类1] (属性1, 形参1){ this.属性1 = 形参1; // 一般这里的形参命名与属性名一致 } }

class Person { private String name; private int age;

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return this.name;
}

public int getAge() {
    return this.age;
}

}

由于构造方法是如此特殊,所以构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。

默认构造方法:如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:

class Person { public Person() { } }

实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例;

没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法;

可以定义多个构造方法,编译器根据参数自动判断;

可以在一个构造方法内部调用另一个构造方法,便于代码复用。

class Person { private String name; private int age;

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

public Person(String name) {
    this(name, 18); // 调用另一个构造方法Person(String, int)
}

public Person() {
    this("Unnamed"); // 调用另一个构造方法Person(String)
}

}

方法重载是指多个方法的方法名相同,但各自的参数不同;
5. 继承:
protected允许子类访问父类的字段和方法;
子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的name和age字段:

class Person { private String name; private int age; }

class Student extends Person { public String hello() { return "Hello, " + name; // 编译错误:无法访问name字段 } }

为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问:

class Person { protected String name; protected int age; }

class Student extends Person { public String hello() { return "Hello, " + name; // OK! } }

//protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问


6. 子类的构造方法可以通过super()调用父类的构造方法:
子类引用父类的字段时,可以用super.fieldName。例如:

class Student extends Person { public String hello() { return "Hello, " + super.name; } }

public class Main { public static void main(String[] args) { Student s = new Student("Xiao Ming", 12, 89);// 编译错误, } }

class Person { protected String name; protected int age;

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

}

class Student extends Person { protected int score;

public Student(String name, int age, int score) {
    this.score = score;
}

}

任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:

class Student extends Person { protected int score;

public Student(String name, int age, int score) {
    super(); // 自动调用父类的构造方法
    this.score = score;
}

}

但是,Person类并没有无参数的构造方法,因此,编译失败。

解决方法是调用Person类存在的某个构造方法。例如:

class Student extends Person { protected int score;

public Student(String name, int age, int score) {
    super(name, age); // 调用父类的构造方法Person(String, int)
    this.score = score;
}

}

这样就可以正常编译了!
如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

7. 区分继承和组合:
在使用继承时,我们要注意逻辑一致性。
考察下面的Book类:

class Book { protected String name; public String getName() {...} public void setName(String name) {...} }

这个Book类也有name字段,那么,我们能不能让Student继承自Book呢?

class Student extends Book { protected int score; }

显然,从逻辑上讲,这是不合理的,Student不应该从Book继承,而应该从Person继承。

究其原因,是因为Student是Person的一种,它们是is关系,而Student并不是Book。实际上Student和Book的关系是has关系。

具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:

class Student extends Person { protected Book book; protected int score; }


因此,继承是is关系,组合是has关系。
yangsirgo commented 3 years ago
  1. 多态:
在子类Student中,覆写这个run()方法:

class Student extends Person { @Override public void run() { System.out.println("Student.run"); } }


- Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如

public void runTwice(Person p) { p.run(); p.run(); }

它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。
多态的特性就是,运行期才能动态决定调用的子类方法。
多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

- final修饰符有多种作用:

1. final修饰的方法可以阻止被覆写;

class Person { protected String name; public final String hello() { return "Hello, " + name; } }

Student extends Person { // compile error: 不允许覆写 @Override public String hello() { } }


2. final修饰的class可以阻止被继承;

final class Person { protected String name; }

// compile error: 不允许继承自Person Student extends Person { }


3. final修饰的field必须在创建对象时初始化,随后不可修改。

class Person { public final String name = "Unamed"; }

Person p = new Person(); p.name = "New Name"; // compile error! //对final字段重新赋值会报错:


//可以在构造方法中初始化final字段:

class Person { public final String name; public Person(String name) { this.name = name; } }


//这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。
yangsirgo commented 3 years ago
  1. 如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
    class Person {
    public abstract void run();
    }

    把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。必须把Person类本身也声明为abstract,才能正确编译它:

    abstract class Person {
    public abstract void run();
    }

无法实例化的抽象类有什么用?

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。

例如,Person类定义了抽象方法run(),那么,在实现子类Student的时候,就必须覆写run()方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run();
    }
}

abstract class Person {
    public abstract void run();
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

面向抽象编程的本质就是:

上层代码只定义规范(例如:abstract class Person);

不需要子类就可以实现业务逻辑(正常编译);

具体的业务逻辑由不同的子类实现,调用者并不关心。

通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;

定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;

如果不实现抽象方法,则该子类仍是一个抽象类;

面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

yangsirgo commented 3 years ago
  1. Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口; 如果一个抽象类没有字段,所有方法全部都是抽象方法:
    abstract class Person {
    public abstract void run();
    public abstract String getName();
    }

    就可以把该抽象类改写为接口:interface。 使用interface可以声明一个接口:

    interface Person {
    void run();
    String getName();
    }

    所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,例如:

class Student implements Person, Hello { // 实现了两个interface
    ...
}

接口继承: 一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

此时,Person接口继承自Hello接口,因此,Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口。 接口也是数据类型,适用于向上转型和向下转型;

接口的所有方法都是抽象方法,接口不能定义实例字段;

接口可以定义default方法(JDK>=1.8)。 实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming");
        p.run();
    }
}

interface Person {
    String getName();
    default void run() {  //在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法:
        System.out.println(getName() + " run");
    }
}

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}
yangsirgo commented 3 years ago
  1. 静态字段和静态方法 在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。 静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:
    class Person {
    public String name;
    public int age;
    // 定义静态字段number:
    public static int number;
    }

用static修饰的方法称为静态方法。调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:

public class Main {
    public static void main(String[] args) {
        Person.setNumber(99);
        System.out.println(Person.number);
    }
}

class Person {
    public static int number;

    public static void setNumber(int value) {
        number = value;
    }
}

因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。

public class Main {
    public static void main(String[] args) {
        Person.setNumber(99);
        System.out.println(Person.number);
    }
}

class Person {
    public static int number;

    public static void setNumber(int value) {
        number = value;
    }
}

接口的静态字段 因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:

public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:

public interface Person {
    // 编译器会自动加上public statc final:
    int MALE = 1;
    int FEMALE = 2;
}
yangsirgo commented 3 years ago

57