- 参考书:《Java语言程序设计与数据结构(基础篇)》—— 梁勇
- 参考视频教程:java教程
- 基于之前的文章,我们已经对java中的类和对象有了初步认识:JAVA入门笔记6 —— 类和对象初步
- 这篇文章将进一步探讨java中面向对象的三大特征
一、封装性
1. 封装的思想
- 封装:指对外部不可见,外部只能通过对象提供的接口来访问。这就好像是一个计算器,我们使用时不需要管它内部是怎样实现计算的,只要关注于使用它的接口(按键)即可
2. 封装的好处
- 封装的好处是
- 良好的封装能够隐藏实现细节,减少耦合
- 封装后可以修改类的内部实现,而无需修改使用了该类的客户代码
- 可以对成员进行更精确的控制
- 这三点里,第一点其实在面向过程中也能由函数体现出来,第二点和第三点则是比较重要的,下面重点分析一下
(1)通过修改类实现,减少用户代码的改动
- 若不进行封装,类外的用户代码会直接访问类成员变量并与之进行交互(就好像C中的结构那样),一旦对类成员变量进行修改,就很可能要修改大量用户代码
- 示例:
-
我们现在有一个学生类,没有使用封装
public class Student { String name; int age; }
在使用它的时候,可以写成这样
Student stu = new Student(); stu.age = 10;
-
现在对类成员变量进行修改
public class Student { String name; String age; }
这时我们不得不修改所有使用
age
成员变量的地方Student stu = new Student(); stu.age = “10”;
如果这个类被用户代码大量使用,将面临很多修改工作
-
现在以封装的思想重写这个类
public class Student { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
-
这时若要修改类成员变量,可以通过修改
getter
和setter
方法对用户代码屏蔽这个变化,从而避免修改大量用户代码public class Student { private String name; private String age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = Integer.toString(age); } public String getName() { return name; } public int getAge() { return Integer.parseInt(age); } }
-
(2)对成员进行更精确的控制
- 进行封装后,我们可以在
setter
方法中对输入进行检查public class Student{ private int age; // 在setter中,可以对数据进行检查 public void setAge(int age) { if(num<100 && num>=0) this.age = num; else System.out.println("数据不合理"); } public int getAge() { return age; } }
- 如果不给某个私有成员设置
setter
方法,可以把这个成员变量变成只读的
3. 在java中使用封装性
- 简单说,就是通过访问权限关键字设置类和类成员的访问权限,禁止类外部直接对私有成员(
private
权限)进行操作,只允许通过公共权限的接口(public
权限的getter
/setter
方法)操作类私有的成员 - 以下主要讨论对类成员变量的封装,当然也可以对成员方法进行封装(比如通过私有化构造函数可以实现单例化编程),不过本文不展开讨论
(1)类访问权限
- java类有两种种访问权限:
默认
/public
默认
:class
前不加任何修饰即为默认权限。该类只能被自身所在的包中的类使用。public
:在class
前添加public
修饰符。该类能被所有包中的类使用。
- 注意:如果在一个源程序文件(
.java
文件)中声明了多个类,只能有一个类的权限关键字是public
,并且这个类的名字应该和程序文件同名,main
方法也应该在这个类中。
(2)成员变量访问权限
-
java类成员变量有四种访问权限:
默认
/public
/protect
/private
,其访问特性如下-
默认
:成员变量前不加任何访问修饰符。该模式下,只允许在同一个包中进行访问扫描二维码关注公众号,回复: 12423705 查看本文章 -
public
:Java语言中访问限制最宽的修饰符。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。虽然java中没有全局变量的概念,但通过public static
修饰,可以近似地实现全局变量// public赋予成员变量全局访问权限,static允许不创建实例地使用它们 public class globalVars { public static String string; public static int a; } // 使用 "全局变量" String tmpString = globalVars.string; int tmpInt = globalVars.a;
-
private
: Java语言中对访问权限限制的最窄的修饰符。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问 -
protect
: 介于public
和private
之间的一种访问修饰符。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问
-
-
小结
(3)权限设置原则
- 类常常是
public
; - 成员变量常常是
private
; - 构造方法一般是
public
; - 方法
getter
与setter
是public
; - 其它方法需要根据实际的需要而定。
二、继承性
1. 继承的思想
-
继承主要解决的问题:
共性抽取
-
举个例子:现在要编写一个企业管理程序,要设置若干职务类,如
讲师
、助教
、保安
等,它们中有一些相同的的成员变量和方法姓名
、工号
…也有自己特有的成员变量和方法- 若不使用继承,我们在每个职务类的实现中都要复制粘贴一遍这些相同的类成员,代码复用性差
- 利用继承性,我们可以定义一个
员工类
作为父类提供共性的类成员,各个职务类作为子类,通过继承父类来获取这些成员,这样实现职务类时就可以专注于编写其特有的类成员,避免了重复的代码,提高了代码复用性。
-
简单来说,继承就是使子类的对象拥有父类的全部属性和行为,同时可以增添自己的所特有的属性和行为。这样可以节省写共同具有的属性和方法代码的时间,有利于代码的复用,这就是继承的基本思想
2. 在java中使用继承性
(1)java中继承的特点
- java语言是单继承的,每个子类有且只能有一个父类。子类的实例都是父类的实例,但不能说父类的实例是子类的实例。
- java语言可以多级继承。继承树中靠近根部的类都是靠近叶的类的父类,但每个类的直接父类只有一个
- 一个父类可以有多个子类
(2)定义和使用子类
-
定义父类:java中定义父类不需要特殊写法,直接定义的类都可以用作父类
-
定义子类:通过
extends
关键字指定父类修饰符 class 子类名 extends 父类名{ 类体 }
-
示例:
-
定义父类
Employee
// Employee.java public class Employee { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void show(){ System.out.println(name+" "+age); } }
-
定义两个子类
Teacher
和Assistant
// Assistant.java public class Assistant extends Employee { }
// Teacher.java public class Teacher extends Employee { }
-
直接使用子类继承到的成员
public class Main { public static void main(String[] args) { Teacher teacher = new Teacher(); teacher.setAge(30); teacher.setName("教师"); teacher.show(); // 教师 30 Assistant assistant = new Assistant(); assistant.setAge(22); assistant.setName("助教"); assistant.show(); // 助教 22 } }
-
(3)继承中成员的访问特性
1. 成员变量的访问特性
-
子类中有成员变量和父类重名,访问重名成员变量时
- 访问特性
- 直接通过
子类实例.成员变量
访问:看定义对象时等号左边
是哪个类,就优先在哪个类中找。即先在子类中找,找不到向上去父类找,还找不到报错 - 使用
子类实例.成员方法
时,成员方法中访问了重名成员变量:看方法属于哪个类
,就优先在哪个类找。若方法是子类的特有方法(非继承)或继承自父类并在在子类覆写了,则优先在子类找,找不到则向上去父类找,还找不到报错;若方法是继承自父类且没有在子类覆写,优先在父类找,找不到向上找,还找不到报错
- 直接通过
- 示例
class Father { int num = 100; public void showNum_F() { System.out.println(num); } } class Son extends Father { int num = 200; public void showNum_S(){ System.out.println(num); } } public class Main { public static void main(String[] args) { Father f = new Father(); Son s = new Son(); System.out.println(f.num); // 100 System.out.println(s.num); // 200 f.showNum_F(); // 100 s.showNum_S(); // 200 s.showNum_F(); // 100 } }
- 访问特性
-
子类成员变量、父类成员变量、方法局部变量三者重名
- 访问特性
- 访问局部变量:直接写变量名
- 访问子类成员变量:
this.变量名
- 访问父类成员变量:
super.变量名
- 示例
class Father { int num = 100; } class Son extends Father { int num = 200; public void show(){ int num = 300; System.out.println(num); // 300 System.out.println(this.num); // 200 System.out.println(super.num); // 100 } } public class Main { public static void main(String[] args) { Son s = new Son(); s.show(); } }
- 访问特性
2. 成员方法的访问特性
-
子类和父类方法重名时
- 通过
子类实例.成员方法
调用时,看定义对象时等号右边
是哪个类(new
的是哪个类),就优先在哪个类中找。即优先在子类找,找不到向上去父类找,一直找不到报错 - 一定要访问父类的同名方法,使用
super
关键字,类似上面关于同名变量的说明
- 通过
-
示例
class Father { public void method(){ System.out.println("父类方法执行"); } public void method_father(){ System.out.println("父类特有方法执行"); } } class Son extends Father { public void method(){ System.out.println("子类方法执行"); super.method(); // 调用父类的同名方法 } } public class Main { public static void main(String[] args) { Son s = new Son(); s.method(); // 子类方法执行 // 父类方法执行 s.method_father(); // 父类特有方法执行 } }
(4)继承中成员方法的覆写
- 概念:在继承的类中重写父类的成员方法,由于继承中子类成员方法的访问特性(“就近原则”),通过子类和父类对象实例调用时会调用到不同的方法
- 思想:当需求改变时,对于已经投入使用的类,尽量不要改动,而是继承定义一个子类,改动和添加新的内容
- 注意
- 必须保证父子类方法名相同,参数列表也相同
- 子类方法的返回值必须小于等于父类方法的返回值范围。这里的小于等于是继承关系上的,比如父类返回
object
变量,子类可以返回String
变量,反之不行 - 子类方法的权限修饰符必须大于等于父类方法。
public > protected > (default) > private
- 正确性检测:可选在子类覆写的方法前写一行注解
@override
,用来检测是否是有效的正确覆盖重写
(5)继承中构造方法的访问特点
- 子类构造时,编译器会在子类构造方法最前面隐式地加上
super();
,调用父类的无参构造方法
若父类中显式定义了有参构造,编译器将不生成隐式无参构造方法,这会导致子类构造方法报错。这时要么在父类显式定义无参构造,要么在子类构造方法最前使用class Father { public Father(){ System.out.println("父类构造"); } } class Son extends Father { public Son(){ System.out.println("子类构造"); } } public class Main { public static void main(String[] args) { Son s = new Son(); } } /* 父类构造 子类构造 */
super(参数)
显式调用父类已有的重载父类构造方法
- 注意:
- 子类中必须使用
super(参数列表)
调用父类的重载构造方法(显式或隐式) - 子类中的
super()
调用必须是子类构造方法的第一个语句(无论显式还是隐式) - 只有子类构造方法中才能使用
super()
调用父类构造方法 - 一个子类构造方法中不能进行多次
super()
调用
- 子类中必须使用
(6)this和super的小结
-
super的三种用法
- 子类成员方法中访问父类成员变量
- 子类成员方法中访问父类成员方法
- 子类构造方法中访问父类构造方法
-
this的三种用法
- 本类成员方法中访问本类成员变量
- 本类成员方法中访问本类另一个成员方法
- 本类构造方法中访问本类另一个构造变量
-
注意:
- 在构造方法中使用
super
和this
,必须放在第一句 - 构造方法中
super
和this
不能同时使用
- 在构造方法中使用
-
内存存储示意
- 示例代码
class Son extends Father { int num = 20; public void method(){ super.method(); System.out.println("子类方法执行"); } public void show() { int num = 30; System.out.println(num); // 30 System.out.println(this.num); // 20 System.out.println(super.num); // 10 } } public class Demo { public static void main(String[] args) { Son s = new Son(); s.show(); s.method(); } }
- 内存示例
- 示例代码
三、多态性
- 多态是建立在继承
extends
和接口实现implements
之上的,继承上面已经探讨过,关于 “抽象和接口” 请参考:JAVA入门笔记8 —— 抽象和接口
1. 多态的思想
- 多态就是对同一个对象,在不同时刻表现出来的不同形态
- 多态允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。比如小明是一个学生,汤姆猫是只猫,但他们都是生物,都会吃饭睡觉。我们可以定义一个吃饭方法,接受一个生物类型的参数,当传入小明时,吃的是披萨;当传入汤姆猫时,吃的是猫粮。这个参数就具有多态性。
2. 在java中使用多态性
(1)多态的定义和使用
- 代码中体现多态性,关键就是:父类引用指向子类对象
- java中父类和子类、接口和实现类之间都可以使用多态性
- 格式:
父类名称 对象名 = new 子类名称(); 接口名称 对象名 = new 实现类名称();
- 这样通过
对象名.方法名()
进行调用时,先在子类/实现类中找,找不到则去父类/接口中找
(2)多态中成员变量的访问特点
-
多态中成员变量的访问特点类似继承中的成员变量访问
- 直接通过对象名称访问:看定义对象时
等号左边
是哪个类,就优先在哪个类中找。即先在父类中找,找不到再向上找 - 使用
对象名.成员方法()
调用时,成员方法中访问了重名成员变量:看方法属于哪个类
,就优先在哪个类找,找不到再向上找。这里和继承有一点区别,就是对象名.成员方法()
这个调用访问不到子类的特殊方法,只能访问到父类特有或子类继承并覆写的方法。如果是在父类特有方法中,优先在父类找;若是在子类覆写的同名方法中,优先在子类找
- 直接通过对象名称访问:看定义对象时
-
示例
class Father{ int num = 10; public void showNum_Father(){ System.out.println(num); } public void showNum(){ System.out.println(num); } } class Son extends Father{ int num = 20; public void showNum_Son(){ System.out.println(num); } public void showNum(){ System.out.println(num); } } public class Main { public static void main(String[] args) { // 多态写法:父类引用指向子类对象 Father obj = new Son(); System.out.println(obj.num); // 同名成员变量优先在等号左边找(父类),打印10 obj.showNum_Father(); // 父类特有方法,优先在父类找,打印10 // obj.showNum_Son(); // 不能通过多态方式访问子类特有方法,报错 obj.showNum(); // 子类覆写的同名方法,优先在子类找,打印20 } }
(3)多态中成员方法的访问特点
- 多态中成员变量的访问特点类似继承中的成员方法访问
- 通过
对象名.成员方法()
调用时,看定义对象时等号右边
是哪个类(new
的是哪个类),就优先在哪个类中找。即优先在子类找,找不到向上去父类找,一直找不到报错 - 注意和继承的区别,访问不到子类特有方法,只能访问到子类覆写的同名方法和父类特有方法
- 通过
- 示例
class Father{ public void method(){ System.out.println("父类同名方法"); } public void methodFather(){ System.out.println("父类特有方法"); } } class Son extends Father{ public void method(){ System.out.println("子类同名方法"); } public void methodSon(){ System.out.println("子类特有方法"); } } public class Main { public static void main(String[] args) { // 多态写法:父类引用指向子类对象 Father obj = new Son(); obj.method(); // 同名方法先在等号右边找(子类) obj.methodFather(); // 父类特有方法去父类找 // obj.methodSon(); // 访问不到子类特有方法,报错 } } /* 子类同名方法 父类特有方法 */
(4)多态的好处
- 现在我们有
Teacher
类和Assisant
类,都是继承自Employee
类/实现了Employee
接口。Employee
中有抽象方法work()
,分别被两个子类覆写。如下
- 若不用多态,就必须定义不同类型的引用变量,分别调用两个work方法,如下
Teacher teacher = new Teacher(); Assisant assisant = new Assisant(); teacher.work(); assiant.work();
- 使用多态,两个引用变量可以统一为
Employee
类的引用变量,如下Employee teacher = new Teacher(); Employee assisant = new Assisant(); teacher.work(); assiant.work();
- 从而我们可以提取出一个
job(Employee employee)
方法,其中调用employee.work()
。这个形参employee
在传入参数不同时表现出不同对象的特性(传入的是实现子类对象,传入时自动向上转型),并在调用work()
时执行了不同的动作。这正是多态的精髓
(5)对象的转型
1. 对象的向上转型
-
含义:创建一个子类对象,把它当作父类来看待和使用
-
格式:对象的向上转型,其实就是多态写法
父类名 对象名 = new 子类名();
-
缺点:依照多态中成员方法的访问特性,对象一旦向上转型为父类,那么就无法使用子类的特有方法
-
注意:
- 向上类型转换一定是安全的。它是从小范围转向大范围。有点类似于基本类型的自动类型转换,如
double num = 100;
- 向上转型之后,可以通过向下转型还原回去
- 向上类型转换一定是安全的。它是从小范围转向大范围。有点类似于基本类型的自动类型转换,如
2. 对象的向下转型
- 含义:对象向上转型的还原动作,将父类对象还原为原来的子类对象
- 格式:类似强制类型转换
子类名称 对象名 = (子类名称)父类对象;
- 注意:对象向下转型时必须转为原来向上转型前的类型(即它创建时的类型),否则报错
ClassCastException
这就类似Animal animal = new Cat(); // 向上转型 Cat cat = (Cat)animal; // 向下转型还原 // Dog dog = (Dat)animal; // 向下转型还原,不同类,报错
int num = (int)10.5;
报错,有精度损失
3. 类型判断
-
上面提到向下转型时必须要转为原来的类型,如何获取原来的类型呢?
-
可以用
instanceof
关键字判断一个父类引用的对象本来是什么子类对象名 instanceof 类名称
这会返回一个
boolean
值,即判断当前对象能不能当作后面类型的实例 -
可以使用
instanceof
结合向下转型使用,避免转型错误Animal animal = new Cat(); // 向上转型 if(animal instanceof Cat){ Cat cat = (Cat)animal; // 向下转型还原 // 调用Cat类特殊方法 } if(animal instanceof Dog){ Dog dog = (Dog)animal; // 向下转型还原 // 调用Dog类特殊方法 }
-
特别是当我们提取出一个方法,使用父类引用形参接受各个子类对象时,
instanceof
关键字非常有用
3. 多态和接口综合示例
-
请先看 “抽象和接口” 相关内容:JAVA入门笔记8 —— 抽象和接口
-
要求:笔记本、键盘、鼠标通过USB接口实现交互
- USB接口:打开设备、关闭设备
- 笔记本类方法:开机、关机、使用USB设备
- 键盘类:实现USB接口,并有按键方法
type
- 鼠标类:实现USB接口,并有点击方法
click
示意图如下
-
代码
-
USB接口
public interface USB { public abstract void open(); public abstract void close(); }
-
键盘类
public class Keyboard implements USB{ @Override public void open() { System.out.println("打开键盘"); } @Override public void close() { System.out.println("关闭键盘"); } public void type(){ System.out.println("按键"); } }
-
鼠标类
public class Mouse implements USB { @Override public void open() { System.out.println("打开鼠标"); } @Override public void close() { System.out.println("关闭鼠标"); } public void click(){ System.out.println("点击"); } }
-
电脑类
public class Computer { public void powerOn(){ System.out.println("电脑开机"); } public void powerOff(){ System.out.println("电脑关机"); } public void useDevice(USB device){ device.open(); if(device instanceof Mouse) ((Mouse) device).click(); else if(device instanceof Keyboard) ((Keyboard) device).type(); device.close(); } }
-
Main类
public class Main { public static void main(String[] args) { Computer computer = new Computer(); Mouse mouse = new Mouse(); Keyboard keyboard = new Keyboard(); computer.powerOn(); computer.useDevice(mouse); computer.useDevice(keyboard); computer.powerOff(); } } /* 电脑开机 打开鼠标 点击 关闭鼠标 打开键盘 按键 关闭键盘 电脑关机 */
-