前一篇简单聊了一下类、对象,还有方法的相关定义和操作,现在来介绍一下面向对象的三大特性:封装、继承和多态。本次内容是我们学习的核心和重点。
封装(Encapsulation),主要针对类或者对象,它将对象的状态和行为(即属性和方法)封装为一个整体,隐藏其内部信息和实现细节,不允许外部直接访问,通过对外暴露方法来实现对内部信息的操作和访问。简言之,即隐藏内部细节,对外暴露接口。
封装在现实生活在也是一种常见且重要的思想,如下:
插座上面有一排排插口,用户在使用插座时不用关心插座内部的电路具体是怎样实现的,而只需要根据暴露在外面的插口型号使用即可。
在使用计算器进行加减乘除时,不用关心内部的加法器工作原理和乘法电路是怎样实现的,只需要根据按键代表的功能使用即可。
我们可以从封装的基本概念中挖掘出封装具有如下优点。
1)安全性
隐藏其内部信息和实现细节,不允许外部直接访问。
2)低耦合性
良好的封装能够减少耦合。
软件工程中有"高内聚低耦合"的概念,是评判软件设计好坏的标准。耦合是指两个子系统(或类)之间的关联程度,关联程度越小,代表耦合度越低。
学过 操作系统(OS) 的同学可以回顾如下知识,没学过的可以简单了解或者不看,仅作为拓展。
关于耦合的理解,如操作系统中的宏内核和微内核。我们都知道,内核是操作系统的核心,如现在的 Linux操作系统正是在 原有Linux内核的基础上发展而来的。宏内核即把所有的功能都耦合起来,放在内核中。好处是性能极高,因为各个功能模块之间可以直接相互调用。但也有明显的坏处,由于各个模块是相互关联的,容易出现一崩全崩。而微内核则恰好相反,内核中只会存放一些核心功能,其余所有功能都会被移出内核,变成一种特殊的用户进程——服务进程,从而降低耦合。
像 Linux、Windows 9X 系列、MacOS 8.6 版本之前都是宏内核的代表,而微内核比如华为的鸿蒙。当然,除了宏内核和微内核,还有混合内核和外内核,有兴趣的可以自行了解。
3)提高了代码的复用性
代码的复用性简单理解就是,某些功能相同且经常用到的代码,可以在通过封装之后直接调用,而不是在后面每次用到时都重复再写一遍那些相同功能的代码,造成代码的冗余和不必要。简言之即功能被封装成了类,通过基类与派生类之间的一些机制(组合和继承),来提高代码的复用性。
4)灵活性
类内部的结构可以自由修改,从而更好地控制类属性和方法。
1)将类变量/属性声明为 private(私有化即只有本类才能访问,其他类不能访问)。
2)提供公共的 getter和 setter方法来访问和更新private 私有变量的值。
public class Student { // 定义一个学生类
private String name; //封装成员变量
private int age;
private String gender;
private String address;
public Student(){ //空参构造
}
public Student(String name,int age,String gender,String address){ //带全部参数的构造
this.name=name; //this指向当前成员变量
this.age=age;
this.gender=gender;
this.address=address;
}
public String getName() { //提供get()和set()方法
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void showInfo(){
System.out.println(this.name+" "+this.age+" "+this.gender+" "+this.address);
}
public void sleep(){ //封装成员方法
System.out.println(name+"正在睡觉");
}
public void study(){
System.out.println(name+"正在学习");
}
public void eat(){
System.out.println(name+"正在吃饭");
}
}
class StudentTest { //测试类
public static void main(String[] args){
//调用带全部参数的构造方法来实例化对象
Student s=new Student("小白",18,"男","南昌");
s.showInfo(); //调用成员方法
s.sleep();
s.study();
s.eat();
System.out.print(s.getName()+" "+s.getAge()+" "+s.getGender()+" "+s.getAddress());
}
}
测试结果
小白 18 男 南昌
小白正在睡觉
小白正在学习
小白正在吃饭
小白 18 男 南昌
封装:对象代表什么,就得封装对应的数据(成员变量),并提供数据对应的行为(成员方法)。如上代码就是完整 JavaBean规范写法。
JavaBean是什么?
JavaBean 是一种JAVA语言写成的可重用组件。其要求类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,并由 set和 get方法获取。
IDEA为我们提供了快速生成 get和 set的方法,Alt+Insert,或者右键选择 Generate,选择所需要提供的方法即可,如下。
继承(Inheritance),是指子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承主要解决的问题:共性抽取,提高代码复用 (复用性主要是可以多次使用,不用再多次写同样的代码,讲封装时也提到过这个概念)
几点注意:
1)子类继承父类,子类就获得了父类的全部功能,只需为子类编写新增的功能即可。
2)子类对父类进行扩展,是从一般到特殊的关系,父类放共性,子类放特性。
3)所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字(extends 和 implements),则默认继承 object祖先类(这个类在 java.lang 包中,所以不需要 import手动导包)
pass:java.lang.Object 类是 Java 语言的根类,任何类都是 Object 类的子类 / 间接子类。
4)类只允许单继承,不允许多继承,支持多重继承(通过接口实现)。
5)子类无法访问父类的 private字段或 private方法,使得继承的作用被削弱了。为了让子类可以访问父类的字段,需要把 private改为 protected。用protected修饰的字段可以被子类访问(关于 protected不理解的可以回去看修饰符部分)。
向上转型和向下转型是子类和父类之间转换的方式。
1)向上转型(upcasting):把一个子类类型安全地变为父类类型(子转父,正常转)
2)向下转型(downcasting):把一个父类类型强制转型为子类类型(父转子,要强转)
为了避免向下转型出错(出现ClassCastException的异常),通常使用 instanceof运算符。
使用注意(重点):
1)判断该对象是否是某一个类 / 子类 / 实现类的实例,如果是,返回 true。
2)instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有继承关系,否则会引起编译错误。(Object getClass() 方法用于获取对象的运行时对象的类)
3)如果一个引用变量为null,那么对任何 instanceof的判断都为false。
eg1:instanceof运算符
public class Test {
public static void main(String[] args) {
Object obj="lishi";
boolean b = obj instanceof String; // true,所有的类都继承于 java.lang.Object
boolean b1 = obj instanceof Object; // true
boolean b2 = obj instanceof Math; // false,java.lang.Math
System.out.println(b);
System.out.println(b1);
System.out.println(b2);
String str = "zhangsan";
// boolean b3 = str instanceof Math; // String 类与 Math 类没有继承关系,编译报错
// getClass()方法获取当前对象的运行时类型
System.out.println(obj.getClass()); // class java.lang.String
boolean b4 = obj.getClass() == String.class; // true
boolean b5 = obj.getClass() == Object.class; // false
System.out.println(b4);
System.out.println(b5);
}
}
eg2:instanceof 和向上/向下转型(需要理解好)
public class Person {
public Person() { // 无参构造
}
}
class Student extends Person { // Student子类
public Student() {
}
}
class Test{ // 测试类
public static void main(String[] args) {
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true
Student n = null;
System.out.println(n instanceof Student); // false
// 利用instanceof,在向下转型(父转子)前可以先判断:
Person p1 = new Student();
System.out.println(p1 instanceof Student); // true
if (p1 instanceof Student) {
// 只有判断成功才会向下转型:
Student s1 = (Student) p1; // 一定会成功
}
// 演示向上转型(子转父,正常转)和向下转型(父转子,要强转):
Student s2 = new Student();
Person p2 = s2; // upcasting, ok 向上转型 Student-->Person-->Object
Object o1 = p2; // upcasting, ok
Object o2 = s2; // upcasting, ok
Person p3 = new Student(); // upcasting, ok
Person p4 = new Person();
Student s3 = (Student) p3; // ok 向下转型,p3实际指向Student实例,转型成功。
// Student s4 = (Student) p4; // 出错,ClassCastException!
//转型失败。p4实际指向Person实例,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
}
}
1)extends:继承类(表示一个类是从另一个类继承的),其中子类是从另一个类继承的类,超类(父类)是从其继承的类。
2)implements:用于实现 interface接口(interface关键字声明接口)。要访问接口方法,接口必须由另一个具有implements关键字(而不是 extends)的类"实现"(类似于继承)。接口方法的主体由"implement"类提供。
pass:final和 this之前介绍过,不懂的可以再回忆回忆。
1)super:实现对父类成员的访问,用来引用当前对象的父类(super.成员变量|成员方法)。
2)this:指向自己的引用(this.成员变量|成员方法)。
3)final:声明类可以把类定义为不能继承的,即最终类;或用于修饰方法,该方法不能被子类重写。
声明类:final class 类名 {}
声明方法:修饰符(public/private/default/protected) final 返回值类型 方法名(){}
eg1:super和 this
public class Person { // Person父类
void eat() {
System.out.println("人吃饭");
}
}
class Man extends Person{ // Man子类
void eat(){
System.out.println("男人吃饭");
}
void eatTest(){
this.eat(); // this 调用自己的方法
super.eat(); // super 调用父类方法
}
}
class Test{ // 测试类
public static void main(String[] args) {
Person p = new Person();
p.eat(); // 人吃饭
Man m = new Man();
m.eatTest();
// 男人吃饭
// 人吃饭
}
}
测试结果
人吃饭
男人吃饭
人吃饭
eg2:接口
设计一个Shape接口和它的两个实现类Square和Circle。要求如下:
1)Shape接口中有一个抽象方法area(),方法接收一个double类型的参数,返回一个double类型的结果。
2)Square和Circle中实现了Shape接口的area()抽象方法,分别求正方形和圆形的面积并返回。
在测试类中创建Square和Circle对象,计算边长为2的正方形面积和半径为3的圆形的面积并返回。
public interface Shape {
public abstract double area(double n); // 抽象方法
}
class Square implements Shape{ // Square实现类
@Override
public double area(double n) {
return n*n;
}
}
class Circle implements Shape{ // Circle实现类
@Override
public double area(double n) {
return Math.PI*n*n;
}
}
class ShapeTest{ // 测试类
public static void main(String[] args) {
Square sq=new Square();
System.out.println("正方形面积为:"+sq.area(2)); // 正方形面积为:4.0
Circle cir=new Circle();
System.out.println("圆形面积为:"+cir.area(3)); // 圆形面积为:28.274333882308138
}
}
eg3:
设计一个学生类 Student和它的一个子类 Undergraduate。要求如下:
1)Student类有 name和 age属性,一个包含两个参数的构造方法,用于给 name和 age属性赋值,一个 show()方法打印Student的属性信息。
2)本科生类 Undergraduate增加一个 degree(学位)属性。有一个包含三个参数的构造方法,前两个参数用于给继承的 name和 age属性赋值,第三个参数给degree专业赋值,一个 show方法用于打印 Undergraduate的属性信息。
3)在测试类中分别创建 Student对象和 Undergraduate对象,调用它们的 show()方法。
public class Student { // 共性抽取,父类 Student
protected String name;
protected int age;
public Student(String name,int age) { // 全参构造
this.name=name;
this.age=age;
}
public Student() { // 无参构造
}
public void showStuInfo() {
System.out.println("姓名:"+name+" "+"年龄:"+age);
}
}
class Undergraduate extends Student{ // 子类Undergraduate
String degree; // 增加一个degree(学位)属性
public Undergraduate(String name, int age,String degree) {
super(name, age);
this.degree=degree; // degree初始化
}
public Undergraduate() {
}
public void showUngrdInfo() {
System.out.println("姓名:"+name+" "+"年龄:"+age+" "+"专业:"+this.degree);
}
}
class StudentTest { // 测试类
public static void main(String[] args) {
Student s=new Student("小明",20); // 调用全参构造方法并完成初始化
s.showStuInfo();
Undergraduate ungrd=new Undergraduate("小明",20,"计算机科学");
ungrd.showUngrdInfo();
}
}
测试结果
姓名:小明 年龄:20
姓名:小明 年龄:20 专业:计算机科学
注意和重载(Overload)的区别,重载在上一篇讲过,不再赘述。
1)重写是什么?
重写(Override),即方法覆盖。是子类对父类的允许访问方法的实现过程进行重新编写,返回值和形参都不能改变(外壳不变,核心重写)
2)为什么要有重写?
解决子类继承父类之后,可能父类的某一个方法不满足子类的具体特征,此时需要重新在子类中定义该方法,并重写方法体。
3)重写的好处?
子类可以根据自身需要,定义特定于自己的行为(即子类能够根据需要实现父类的方法)
三同:
1)实例方法签名必须相同。
方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。注:方法签名不包括方法的返回类型、返回值和访问修饰符。
2)子类方法的返回值类型和父类方法的返回类型相同或者是其子类。
3)子类方法声明抛出的异常类和父类方法声明抛出的异常类相同或者是其子类(运行时异常除外)
一大:
子类方法的访问权限比父类方法访问权限更大或相等(防止父类方法失传)
若需要在子类中调用父类的被重写方法,要使用 super关键字。
public class Person {
public void think(){
System.out.println("人在思考");
}
}
class Student extends Person{
public void think(){ //重写
// 在子类中调用父类的被重写方法,使用super。
super.think(); // 应用父类的方法
System.out.println("学生在想问题");
}
public void learn(){
System.out.println("学生在学习");
}
}
class TestStudent{ // 测试类
public static void main(String[] args) {
Person p=new Person(); // Person对象
Person s=new Student(); // Student对象,向上转型(子转父)正常转
p.think(); // 执行 Person类的方法
System.out.println("------");
s.think(); // 执行 Student类的方法
// s.learn(); // 编译错误,因为s的引用类型 Person没有 learn方法
Student s1=new Student();
s1.learn();
}
}
测试结果
人在思考
------
人在思考
学生在想问题
学生在学习
多态(Inheritance),即多种形态,是指是同一个行为具有多个不同表现形式或形态的能力。
产生于编译时类型和运行时类型不一致时。
1)编译时类型:由声明该变量时使用的类型决定。
2)运行时类型:由实际赋给该变量的对象决定。
1)有继承/实现关系。
2)有父类引用指向子类对象:Parent p=new Child(); (子转父,向上转型,正常转)
3)有方法的重写。
1)好处
为什么要使用"继承"和"多态性"? 因为它对于代码的可重用性很有用(在创建新类时可以重用现有类的属性和方法)。
a、使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。
b、消除类型之间的耦合关系。
c、其他:可替换性、可扩充性、接口性、灵活性、简化性。
2)缺点
不能使用子类的特有功能。
这里指的是引用数据类型。
1)自动类型转换
2)强制类型转换(强转)
1)可以转换成真正的子类类型,从而调用子类独有功能。
2)转换类型与真实对象类型不一致会报错。
3)转换的时候用instanceof关键字进行判断。
因为它对于代码的可重用性很有用:在创建新类时可以重用现有类的属性和方法。
几点注意:
1)引用类型之间的转换只能在具有继承关系的两个类型之间进行,否则编译报错。
2)只能将一个引用变量的类型强制转换成该变量实际引用的对象可以被定义成的类型,否则会引发 ClassCastException 异常
语法格式:(Type)object
public class Person {
public Person() { // 无参构造
}
}
class Student extends Person { // Student子类
public Student() {
}
}
class Teacher extends Person{ // Teacher子类
public Teacher(){
}
}
class Test{ // 测试类
public static void main(String[] args) {
// 向上转型:把子类对象赋给父类引用变量(多态)
Person p=new Student();
Object obj=new Student();
// 强制类型转换:把父类对象赋给子类引用变量
Student s=(Student) p;
// Teacher t=(Teacher) p; // 出错,ClassCastException: Student cannot be cast to Teacher
// Teacher t1=(Teacher) new Person(); // 出错,ClassCastException: Person cannot be cast to Teacher
// 在进行向下转型(强转)之前,先用instanceof 判断是否可成功转换,从而避免出现 ClassCastException异常
if (p instanceof Student) {
System.out.println(p instanceof Student); // true
Student s1 = (Student) p;
}
}
}
pass:这次的内容有点多,需要好好消化吸收,主要把代码看懂的话就理解了。最后希望能帮助到大家,本人文笔不是很好,如有表述不清楚或者遗漏的地方,望见谅!