封装:把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。

继承:从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

多态:多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

类的封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter方法来查看和设置变量。 封装就是通过访问权限控制来实现的。

public class Person {
	private String name;
	private int age;
	private String sex;
 
	public Person(String name, int age, String sex) {
		this.name = name;
		this.age = age;
		this.sex = sex;
	}
	public String getName() {
		return name;
	}
	public String getSex() {
		return sex;
	}
	public int getAge() {
		return age;
	}
}

我们甚至还可以将构造方法改成私有的,需要通过我们的内部的方式来构造对象, 进而实现单例模式, 即全局只能构造一次该对象:

public class Test {
    private static Test instance;
    
    private Test(){}
    
    public static Test getInstance() {
        if(instance == null) 
            instance = new Test();
        return instance;
    }
}

类的继承

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。

比如说我们一开始使用的人类,那么实际上人类根据职业划分,所掌握的技能也会不同,比如画家会画画,歌手会唱,舞者会跳,Rapper会rap,运动员会篮球,我们可以将人类这个大类根据职业进一步地细分出来:

实际上这些划分出来的类,本质上还是人类,也就是说人类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的技能。

我们可以创建各种各样的子类,想要继承一个类,我们只需要使用extends关键字即可:

public class Worker extends Person {
	
}

类的继承可以不断向下,但是同时只能继承一个类,同时,标记为final的类不允许被继承:

public final class Person {
 
}

当一个类继承另一个类时,属性和方法会被继承,可以直接访问父类中定义的属性和方法,除非父类中将属性和方法的访问权限修改为private,那么子类将无法访问(但是依然是继承了这个属性的)。当然, 如果是 protect 时还是能访问的, 默认也不行。

如果父类存在一个有参构造方法,子类必须在构造方法中调用

public class Person {
    protected String name;   //因为子类需要用这些属性,所以说我们就将这些变成protected,外部不允许访问
    protected int age;
    protected String sex;
    protected String profession;
 
      //构造方法也改成protected,只能子类用
    protected Person(String name, int age, String sex, String profession) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.profession = profession;
    }
 
    public void hello(){
        System.out.println("["+profession+"] 我叫 "+name+",今年 "+age+" 岁了!");
    }
}

对于子类 Worker:

public class Worker extends Person {
	public Worker() {
		// 仅有子类的构造方法是不行的
	}
}

因为子类在构造时,不仅要初始化子类的属性,还需要初始化父类的属性,所以说在默认情况下,子类其实是调用了父类的构造方法的,只是在无参的情况下可以省略,但是现在父类构造方法需要参数,那么我们就需要手动指定了。

既然现在父类需要三个参数才能构造,那么子类需要按照同样的方式调用父类的构造方法:

public class Worker extends Person{
    public Worker(String name, int age, String sex) {
        super(name, age, sex, "工人");    //父类构造调用必须在最前面
        System.out.println("工人构造成功!");    //注意,在调用父类构造方法之前,不允许执行任何代码,只能在之后执行
    }
}

虽然我们这里使用的是父类类型引用的对象,但是这并不代表子类就彻底变成父类了,这里仅仅只是当做父类使用而已。

我们也可以使用强制类型转换,将一个被当做父类使用的子类对象,转换回子类:

public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");
    Student student = (Student) person;   //使用强制类型转换(向下转型)
    student.study();
}

但是注意,这种方式只适用于这个对象本身就是对应的子类才可以,如果本身都不是这个子类,或者说就是父类,那么会出现问题。

那么如果我们想要判断一下某个变量所引用的对象到底是什么类,那么该怎么办呢? 用 instanceof 判断对象属于什么类

我们发现,除了我们自己在类中编写的方法之外,还可以调用一些其他的方法,那么这些方法不可能无缘无故地出现,肯定同样是因为继承得到的,那么这些方法是继承谁得到的呢?

这些方法就来自于顶层Object类

因此也可以解释为什么 println 在源码中传入的是 Object x 类型的参数。而我们传入 String 时也可以正常输出:

public void println(Object x) {
    String s = String.valueOf(x);   //这里同样会调用对象的toString方法,所以说跟上面效果是一样的
    synchronized (this) {
        print(s);
        newLine();
    }
}

因为所有类型都是继承自Object,如果方法接受的参数是一个引用类型的值,那只要是这个类的对象或是这个类的子类的对象,都可以作为参数传入。

方法的重写

法的重写不同于之前的方法重载,不要搞混了,方法的重载是为某个方法提供更多种类,而方法的重写是覆盖原有的方法实现,比如我们现在不希望使用Object类中提供的equals方法,那么我们就可以将其重写了:

public class Person{
    ...
 
    @Override   //重写方法可以添加 @Override 注解,有关注解我们会在最后一章进行介绍,这个注解默认情况下可以省略
    public boolean equals(Object obj) {   //重写方法要求与父类的定义完全一致
        if(obj == null) return false;   //如果传入的对象为null,那肯定不相等
        if(obj instanceof Person) {     //只有是当前类型的对象,才能进行比较,要是都不是这个类型还比什么
            Person person = (Person) obj;   //先转换为当前类型,接着我们对三个属性挨个进行比较
            return this.name.equals(person.name) &&    //字符串内容的比较,不能使用==,必须使用equals方法
                    this.age == person.age &&       //基本类型的比较跟之前一样,直接==
                    this.sex.equals(person.sex);
        }
        return false;
    }
}

这里之所以判断 obj 属于 Person 类型后还要转换类型, 是因为 instancecof 满足是当前类或者是当前类的子类都可以成立。

有时候为了方便查看对象的各个属性,我们可以将Object类提供的toString方法重写了:

@Override
public String toString() {    //使用IDEA可以快速生成
    return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", sex='" + sex + '\'' +
            ", profession='" + profession + '\'' +
            '}';
}

注意,静态方法不支持重写,因为它是属于类本身的,但是它可以被继承。

基于这种方法可以重写的特性,对于一个类定义的行为,不同的子类可以出现不同的行为,比如考试,学生考试可以得到A,而工人去考试只能得到D:

public class Student extends Person{
    ...
 
    @Override
    public void exam() {
        System.out.println("我是学生,我就是小镇做题家,拿个 A 轻轻松松");
    }
}
public class Worker extends Person{
    ...
 
    @Override
    public void exam() {
        System.out.println("我是工人,做题我并不擅长,只能得到 D");
    }
}

这其实就是面向对象编程中多态特性的一种体现。

注意,我们如果不希望子类重写某个方法,我们可以在方法前添加final关键字,表示这个方法已经是最终形态:

public final void exam(){
    System.out.println("我是考试方法");
}

我们在重写父类方法时,如果希望调用父类原本的方法实现,那么同样可以使用super关键字:

@Override
public void exam() {
    super.exam();   //调用父类的实现
    System.out.println("我是工人,做题我并不擅长,只能得到 D");
}

然后就是访问权限的问题,子类在重写父类方法时,不能降低父类方法中的可见性, 因为子类实际上可以当做父类使用,如果子类的访问权限比父类还低,那么在被当做父类使用时,就可能出现无视访问权限调用的情况,这样肯定是不行的,但是相反的,我们可以在子类中提升权限:

protected void exam(){ // 父类
    System.out.println("我是考试方法");
}
@Override
public void exam() {   //将可见性提升为public 
    System.out.println("我是工人,做题我并不擅长,只能得到 D");
}