类学习笔记——【类的 封装、继承和多态】
@
源码:
Gitee https://gitee.com/drip123456/java-se
GIthub https://github.com/Drip123456/JavaSE
专栏: JavaSE笔记专栏
封装、继承和多态
封装、继承和多态是面向对象编程的三大特性。
封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。
多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。
正是这三大特性,让我们的Java程序更加生动形象。
类的封装
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用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; //想要知道这个对象的名字,必须通过getName()方法来获取,并且得到的只是名字值,外部无法修改
}
public String getSex() {
return sex;
}
public int getAge() {
return age;
}
}
我们可以来试一下:
public static void main(String[] args) {
Person person = new Person("小明", 18, "男");
System.out.println(person.getName()); //只能通过调用getName()方法来获取名字
}
也就是说,外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字:
public void setName(String name) {
if(name.contains("小")) return;
this.name = name;
}
我们甚至还可以将构造方法改成私有的,需要通过我们的内部的方式来构造对象:
public class Person {
private String name;
private int age;
private String sex;
private Person(){} //不允许外部使用new关键字创建对象
public static Person getInstance() { //而是需要使用我们的独特方法来生成对象并返回
return new Person();
}
}
通过这种方式,我们可以实现单例模式:
public class Test { private static Test instance; private Test(){} public static Test getInstance() { if(instance == null) instance = new Test(); return instance; } }
单例模式就是全局只能使用这一个对象,不能创建更多的对象,我们就可以封装成这样,关于单例模式的详细介绍,还请各位小伙伴在《Java设计模式》视频教程中再进行学习。
封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。
类的继承
前面我们介绍了类的封装,我们接着来看一个非常重要特性:继承。
在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。
比如说我们一开始使用的人类,那么实际上人类根据职业划分,所掌握的技能也会不同,比如画家会画画,歌手会唱,舞者会跳,Rapper会rap,运动员会篮球,我们可以将人类这个大类根据职业进一步地细分出来:
实际上这些划分出来的类,本质上还是人类,也就是说人类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的技能。在Java中,我们可以创建一个类的子类来实现上面的这种效果:
public class Person { //先定义一个父类
String name;
int age;
String sex;
}
接着我们可以创建各种各样的子类,想要继承一个类,我们只需要使用extends
关键字即可:
public class Worker extends Person{ //工人类
}
public class Student extends Person{ //学生类
}
类的继承可以不断向下,但是同时只能继承一个类,同时,标记为final
的类不允许被继承:
public final class Person { //class前面添加final关键字表示这个类已经是最终形态,不能继承
}
当一个类继承另一个类时,属性会被继承,可以直接访问父类中定义的属性,除非父类中将属性的访问权限修改为private
,那么子类将无法访问(但是依然是继承了这个属性的):
public class Student extends Person{
public void study(){
System.out.println("我的名字是 "+name+",我在学习!"); //可以直接访问父类中定义的name属性
}
}
同样的,在父类中定义的方法同样会被子类继承:
public class Person {
String name;
int age;
String sex;
public void hello(){
System.out.println("我叫 "+name+",今年 "+age+" 岁了!");
}
}
子类直接获得了此方法,当我们创建一个子类对象时就可以直接使用这个方法:
public static void main(String[] args) {
Student student = new Student();
student.study(); //子类不仅有自己的独特技能
student.hello(); //还继承了父类的全部技能
}
是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。
如果父类存在一个有参构造方法,子类必须在构造方法中调用:
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+" 岁了!");
}
}
可以看到,此时两个子类都报错了:
因为子类在构造时,不仅要初始化子类的属性,还需要初始化父类的属性,所以说在默认情况下,子类其实是调用了父类的构造方法的,只是在无参的情况下可以省略,但是现在父类构造方法需要参数,那么我们就需要手动指定了:
既然现在父类需要三个参数才能构造,那么子类需要按照同样的方式调用父类的构造方法:
public class Student extends Person{
public Student(String name, int age, String sex) { //因为学生职业已经确定,所以说学生直接填写就可以了
super(name, age, sex, "学生"); //使用super代表父类,父类的构造方法就是super()
}
public void study(){
System.out.println("我的名字是 "+name+",我在学习!");
}
}
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, "男"); //这里使用父类类型的变量,去引用一个子类对象(向上转型)
person.hello(); //父类对象的引用相当于当做父类来使用,只能访问父类对象的内容
}
虽然我们这里使用的是父类类型引用的对象,但是这并不代表子类就彻底变成父类了,这里仅仅只是当做父类使用而已。
我们也可以使用强制类型转换,将一个被当做父类使用的子类对象,转换回子类:
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
Student student = (Student) person; //使用强制类型转换(向下转型)
student.study();
}
但是注意,这种方式只适用于这个对象本身就是对应的子类才可以,如果本身都不是这个子类,或者说就是父类,那么会出现问题:
public static void main(String[] args) {
Person person = new Worker("小明", 18, "男"); //实际创建的是Work类型的对象
Student student = (Student) person;
student.study();
}
此时直接出现了类型转换异常,因为本身不是这个类型,强转也没用。
那么如果我们想要判断一下某个变量所引用的对象到底是什么类,那么该怎么办呢?
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
if(person instanceof Student) { //我们可以使用instanceof关键字来对类型进行判断
System.out.println("对象是 Student 类型的");
}
if(person instanceof Person) {
System.out.println("对象是 Person 类型的");
}
}
如果变量所引用的对象是对应类型或是对应类型的子类,那么instanceof
都会返回true
,否则返回false
。
最后我们需要来特别说明一下,子类是可以定义和父类同名的属性的:
public class Worker extends Person{
protected String name; //子类中同样可以定义name属性
public Worker(String name, int age, String sex) {
super(name, age, sex, "工人");
}
}
此时父类的name属性和子类的name属性是同时存在的,那么当我们在子类中直接使用时:
public void work(){
System.out.println("我是 "+name+",我在工作!"); //这里的name,依然是作用域最近的哪一个,也就是在当前子类中定义的name属性,而不是父类的name属性
}
所以说,我们在使用时,实际上这里得到的结果为null
:
那么,在子类存在同名变量的情况下,怎么去访问父类的呢?我们同样可以使用super
关键字来表示父类:
public void work(){
System.out.println("我是 "+super.name+",我在工作!"); //这里使用super.name来表示需要的是父类的name变量
}
这样得到的结果就不一样了:
但是注意,没有super.super
这种用法,也就是说如果存在多级继承的话,那么最多只能通过这种方法访问到父类的属性(包括继承下来的属性)