封装:把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
继承:从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。
多态:多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。
类的封装
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用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");
}