第4章 - Java面向对象
作者:vwFisher
时间:2019-09-04
GitHub代码:https://github.com/vwFisher/JavaBasicGuide
目录
1 包
(basic.facobj.pack.PackageDemo)
- 对类文件进行分类管理
- 给类提供多层命名(名称)空间
- 写在程序文件的第一行
- 类名的全称: 包名.类名
- 包也是一种封装形式
包与包间的类进行访问, 被访问的包中的类必须是public的, 被访问的包中的类的方法也必须是public的
public | protected | default(缺省) | private | |
---|---|---|---|---|
同一类中 | ok | ok | ok | ok |
同一包中 | ok | ok | ok | |
子类中 | ok | ok | ||
不同包中 | ok |
2 对象Object类
(basic.facobj.obj.ObjectDemo)
Object: 所有类的根类.(类默认继承Object)。Object是不断抽取而来, 具备着所有对象都具备的共性内容.
所有的类都会从类Object中继承成员. 它们都是方法, 其中有7个是public访问类型, 两个是protected访问类型. 如表所示
方法 | 目的 |
---|---|
toString() |
[可覆盖实现] 返回一个描述当前对象的String对象。默认实现:getClass().getName() + '@' + Integer.toHexString(hashCode()) |
equals() |
[可覆盖实现] 传入的对象的引用与指向当前对象的引用进行比较 (即 对象引用 和 参数都相同才返回 true) |
getClass() | 返回一个标识当前对象所属类的Class类型的对象 |
hashCode() |
[可覆盖实现] 计算对象哈希代码值,以int类型返回。java.util包中定义的类用哈希代码值在哈希表中存储对象 |
notify() | 用于唤醒一个与当前对象关联的线程 |
notifyAll() | 用于唤醒与当前对象关联的所有线程 |
wait() | 等待,当前线程会释放锁, 直到其他线程调用当前对象的 notify() / notifyAll() 方法唤醒 |
注意 getClass()、notify()、notifyAll()、wait() 方法无法被覆盖,它们在Object类定义中被final关键字固定
还有两个protected方法如下表所示
方法 | 目的 |
---|---|
clone() |
[可覆盖实现] 克隆,创建当前对象的一个副本对象。注:浅克隆和深克隆区别。不是对所有类对象都有用,并且不总是能进行所期望的操作 |
finalize() |
[可覆盖实现] 当对象被销毁时,这个方法可以用来进行清理工作。不保证确定执行回收!主要是因为:调用时间不确定、可能不被调用、对象可能在finalize函数调用时复活、要记得自己做异常捕获、小心线程安全 |
2.1 判断对象的类型
(basic.facobj.obj.GetClassDemo)
从 Object 类继承而来的 getClass() 方法会返回一个 Class 类型的对象(该对象所属的类)。可以通过 getName:获取Class对象的完全限定名称。
在程序运行过程中,Class 实例表示程序中的每个类和接口。当程序加载时,Java虚拟机(Java Virtual Machine,JVM)会生成这些内容,因为 Class 主要由 JVM 使用,所以它没有公共构造函数,因此不能自行创建 Class 类型的对象。
getClass() 方法获得特定类或特定接口类型对应的 Class 对象,也可以在任何类、接口或基本类型的名称后附加 .class
会获得对应 Class 对象的引用。如:int.class是基本类型int的类对象。可以使用该方法来判断 Class 的类型
if (pet.getClass() == Cat.class) {
System.out.println("当前pet是Cat");
}
注意: Class 类不是普通类,而是泛型类的实例,Class只是定义了类的集合。在程序中使用的每个类、接口、数组以及基本类型,都由来自 Class 泛型类定义的集合中、某个唯一类的对象来表示
还有一个 instanceof 运算符,它的功能于此几乎相同,但是有一定区别!
instanceof 运算符检查对象转换中, 左操作数引用的对象转换到右操作数设定的类型是否合法. 如果对象与右操作数的类型相同或是其任意子类型, 结果为 true.
// instanceof
if (pet instanceof Animal) {
System.out.println("当前pet是Animal派生类");
}
if (pet instanceof Cat) {
System.out.println("当前pet是Cat");
}
if (pet instanceof Pig) {
System.out.println("当前pet是Pig");
}
2.2 复制对象
(basic.faceobj.CloneDemo)
从 Object 类中有个 clone() 方法,用于创建了一个作为当前对象的复本的新对象。只要被克隆的对象显式支持克隆(实现 Cloneable 接口)时,该方法有效。否则抛出 CloneNotSupportedException 异常。定义如下
protected native Object clone() throws CloneNotSupportedException
一、可以发现 Cloneable 源码,里面是空实现。实现该接口 表示该类,支持clone()调用,否则抛出 CloneNotSupportedException 异常
二、克隆2个说明的点
- 拷贝对象返回的是一个新对象,而不是一个引用
- 拷贝对象与用 new 操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象具有的信息,而不是对象的初始信息(可以看 CloneDemo 看浅克隆和深克隆的区别)
三、注意的几个点
- 覆盖 clone() 的对象所属的类 Class 必须 implements Clonable 接口 (java.lang.Cloneable)
- 在 clone() 方法中调用 super.clone(), 完成了内存块的新建并赋值功能. 这也意味着无论clone类的继承结构是什么样的, super.clone() 直接或间接调用了 java.lang.Object 类的 clone() 方法
- 什么时候使用 shallow Clone, 什么时候使用 deep Clone, 这个主要看具体对象的域是什么性质的, 基本类型 还是 reference variable。clone 方法,只是简单的基本数据类型的赋值(浅克隆),而对于引用类型,clone 就不能完全拷贝一个全新副本出来。这时候必须手动对自定义类的属性拷贝(深克隆)
以 Employee 为例, 它里面有一个域 hireDay 不是基本型别的变量, 而是一个 reference 变量, 经过 Clone 之后就会产生一个新的 Date 类型的 reference, 它和原始对象中对应的域指向同一个 Date 对象, 这样克隆类就和原始类共享了一部分信息, 而这样显然是不利的, 过程如图所示
clone-desc.png2.3 hashCode 与 equals
hashCode() 的作用是获取哈希码, 也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode
在 HashSet 和 HashMap 的 key 中,他们是会先计算 对象的 hashCode 来判断对象加入到位置。而判断重复的依据是 hashCode 相等,且 equals() 验证对象是否真的相同。如果相同,则不执行加入操作,如果不同,则重新散列到其他位置。
所以 hashCode() 获取到的 哈希码,也称为散列码,是用来确定对象在哈希表的索引位置的。
hashCode() 与 equals()的相关规定
- 如果两个对象相等,则 hashCode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashCode 值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
推荐阅读:Java hashCode() 和 equals()的若干问题解答
java.util.Objects
对于 equals 判断,更推荐使用 java.util.Objects#equals(JDK7 引入的工具类)。
Objects.equals(null,"SnailClimb");// false
2.4 序列化 Serializable
Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
3 面向对象
面向对象和面向过程区别,如:大象装进冰箱这个动作
- 面向过程(强调过程):1).打开冰箱 -> 2).存储大象 -> 3).关闭冰箱
- 面向对象(强调对象):1).冰箱打开 -> 2).冰箱存储 -> 3).冰箱关闭
特点:
- 面向对象就是一种常见的思想. 符合人们的思考习惯
- 面向对象的出现, 将复杂的问题简单化
- 面向对象的出现, 让曾经在过程中的执行者变成了对象中的指挥者.
3.1 类与对象
3.1.1 什么是对象?
在计算机的世界中,面向对象程序设计的思想要以对象来思考问题,首先要将现实世界的实体抽象成对象,考虑这个对象具备的属性和行为。
比如:一致大雁要从北方飞往南方这样一个问题的分析
- 将现实生活中见到的事物抽象成一个对象:这里抽象出的对象为大雁
- 识别这个对象的属性/静态属性:大雁有一堆翅膀, 脚, 嘴, 黑色的羽毛等
- 识别这个对象的动态行为, 抽象为方法:飞行, 觅食等
- 然后封装成一个对象:具有大雁的静态属性和动态行为的方法就可以了。
- 再究其本质, 可以将这些属性和行为封装起来描述鸟类,
类实际上就是封装对象属性和行为的载体, 而对象则是类抽象出来的一个实例
3.1.2 什么是类?
类就是同一类事物的统称, 例如上面例子的鸟类就是一个统称, 并且拥有鸟类都具备的属性和行为方法, 而大雁是实例出来的对象. 即: 类是某个事物抽象统称, 对象就是这个事物相对应的实体
3.1.3 面向对象的特点
封装
封装是面向对象编程的核心思想,将对象的属性和行为封装起来变成一个类(类就是载体),类通常对客户隐藏其实现细节(封装的思想)。
采用封装的思想,类将内部数据隐藏,只为用户提供对象的属性和行为的接口,用户通过这些接口使用这些类,无须知道这些类内部时如何构成的。不能操作类中的内部数据。避免外部对内部数据的影响,提高程序的可维护性
继承
继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
例如,鸟类,有大雁,信鸽,可以统一都继承与鸟类,复用已经定义好的类来减少冗余
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
多态存在的三个必要条件:
- 要有继承 extend
- 要有重写 @Override
- 父类引用指向子类对象。
3.1.4 类与对象
(basic.facobj.CarDemo)
用java语言对现实生活中的事物进行描述(通过类的形式来体现的)
- 对于事物描述两方面: 一个是属性, 一个是行为
- 只要明确该事物的属性和行为并定义在类中即可
对象: 就是该类事物实实在在存在的个体, 该类事物的实例, 在Java中通过new来创建
类: 事物的描述。定义类其实就是在定义类中的成员,成员包括: 成员变量(属性), 成员函数(行为)。如:用Java语言来描述小汽车,抽象属性:轮胎数,颜色,抽象行为:运行
成员变量和局部变量的区别:
成员变量 | 局部变量 |
---|---|
定义在类中, 整个类中都可以访问 | 定义在函数, 语句, 局部代码块中, 只在所属的区域有效 |
存在于堆内存的对象中 | 存在于栈内存的方法中 |
随着对象的创建而存在, 随着对象的消失而消失 | 随着所属区域的执行而存在, 随着所属区域的结束而释放 |
有默认初始化值 | 没有默认初始化值 |
对象在内存中的体现
一、例1:
Car c = new Car();
c.num = 4;
c.color = "red";
c.run();
- main压栈,c压栈
- 堆中开辟0x0034地址指向c, 指向一个引用数据变量, 并默认初始化值(int为0, color为null)(有多个地址就开辟多个地址指向对象)
- 赋值c这个对象的num=4, 和color=red
二、例2:
Car c1 = new Car();
Car c2 = c1;
c1.num = 8;
c2.color = "red";
c1.run(); // 8...red
- main压栈,c1压栈
- 堆中开辟0x0034地址指向c1, 指向一个引用数据变量, 并默认初始化值(int为0, color为null)(有多个地址就开辟多个地址指向对象)
- c2压栈, 与c1一样指向同一个地址0x0034
- 修改c1.num的值=8
- 修改c2.color=red
- 执行run发现输出 8...red(说明的确是同一个地址里面的对象修改)
3.2 类
3.2.1 类的构造方法
(basic.faceobj.ConstructorDemo)
在类中除了成员方法之外,还存在一种特殊类型的方法,那就是构造方法。
构造方法是一个与类同名的方法,对象的创建就是通过构造方法完成的,每当实例化一个对象时,类都会自动调用构造方法
1.特点
- 函数名与类名相同
- 不用定义返回值类型
- 没有具体的返回值
2.作用: 给对象进行初始化
3.格式:
构造方法修饰符 构造方法的名称(与类名一致) { 构造方法内容 }
public className() { }
4.注意:
- 默认构造函数的特点
- 多个构造函数是以重载的形式存在的
- 一个类中如果没有定义过构造函数,那么该类中会有一个默认的空参数构造函数
- 如果在类中定义了指定的构造函数,那么类中的默认构造函数就没有了
5.构造函数与一般函数的区别?
- 构造函数:对象创建时,就会调用与之对应的构造函数,对对象进行初始化,只调用一次
- 一般函数:对象创建后,需要函数功能时才调用,可以被调用多次
6.什么时候定义构造函数呢?
在描述事物时,该事物一存在就具备的一些内容,这些内容都定义在构造函数中。构造函数可以有多个,用于对不同的对象进行针对性的初始化,多个构造函数在类中是以重载的形式来体现的
7.细节
- 构造函数如果完成了set功能,set方法是否需要
- 一般函数不能直接调用构造函数
- 构造函数如果前面加了void就变成了一般函数
- 构造函数中是有return语句的
8.子父类中的构造函数的特点
在子类构造对象时,访问子类构造函数时,父类也运行了。为什么呢?
原因是:在子类的构造函数中第一行有一个默认的隐式语句。
super();
子类的实例化过程:子类中所有的构造函数默认都会访问父类中的空参数的构造函数。
9.为什么子类实例化的时候要访问父类中的构造函数呢?
因为子类继承了父类,在使用父类时,需要知道父类时如何初始化,以获取到了父类中内容(属性),所以子类在构造对象时,必须访问父类中的构造函数。在子类的构造函数中加入了super()语句。
如果父类中没有定义空参数构造函数,那么子类的构造函数必须用super明确要调用父类中某个构造函数。同时子类构造函数中如果使用this调用了本类构造函数时,那么super就没有了,因为super和this都只能定义第一行。所以只能有一个,是可以保证的是,子类中肯定会有其他的构造函数访问父类的构造函数。
注意: super语句必须要定义在子类构造函数的第一行. 因为父类的初始化动作要先完成。
10.一个对象实例化过程
Person p = new Person();
- JVM读取指定路径的Person.class文件,加载进内存,并会先加载Person的父类(有直接的父类的情况)
- 在堆内存中的开辟空间,分配地址。
- 并在对象空间中,对对象中的属性进行默认初始化。
- 调用对应的构造函数进行初始化。
- 在构造函数中,第一行会先调用父类中构造函数进行初始化。
- 父类初始化完毕后,再对子类的属性进行显示初始化。
- 在进行子类构造函数的特定初始化。
- 初始化完毕后,将地址值赋值给引用变量。
3.2.2 类的主方法
主方法是类的入口点,它定义了程序从何处开始:主方法提供对程序流向的控制,Java编译器通过主方法来执行程序. 主方法的格式如下:
public static void main(String[] args) { }
主函数特殊之处:
- 格式是固定的
- 被JVM所识别和调用.
- 主方法是静态的, 要直接在主方法中调用其他方法, 则该方法必须也是静态的
- 主方法没有返回值
- 主方法的形参为数组
- public: 因为权限必须是最大的.
- static: 不需要对象的, 直接用主函数所属类名调用即可.
- void: 主函数没有具体的返回值.
- main: 函数名, 不是关键字, 只是一个JVM识别的固定的名字.
- String[] args: 主函数的参数列表, 数组类型的参数, 而且元素都是字符串类型.
3.2.3 成员变量
类的属性称为成员变量,其实成员变量就是普通的变量,可以设置初始值,也可以不设置,系统会默认初始化值。
成员变量前面还会赋有private、public、protected关键字,甚至没有,设置的关键字对应的作用范围不同。
对于成员变量中,父类和子类如何区分?
当本类的成员和局部变量同名用this区分,当子父类中的成员变量同名用super区分父类。
- this:代表一个本类对象的引用
- super:代表一个父类空间
3.2.4 成员方法(函数)
成员方法对应类对象的行为,如 CarDemo 含有相关的 getter、setter 方法 和 run() 方法,格式如下:
权限修饰符 返回值类型 方法名(参数类型 参数名) {
// 执行代码
return 返回值;
}
当子父类中出现成员函数一模一样的情况,会运行子类的函数,这种现象,称为覆盖操作。这是函数在子父类中的特性。函数两个特性:
- Overload(重载) 同一个类中
- Override(覆盖) 在子类中
覆盖注意事项:
- 子类方法覆盖父类方法时,子类权限必须要大于等于父类的权限。(例如父类是HashMap,子类只能大于HashMap,用HashMap或Map,详细见里氏替换原则)
- 静态只能覆盖静态,或被静态覆盖。覆盖的使用:当对一个类进行子类的扩展时,子类要保留父类的功能声明,但是要定义子类中该功能的特有内容时
3.2.5 局部变量
在成员方法内定义的变量,被称为局部变量,在方法被执行时创建,方式执行结束时销毁,局部变量在使用时必须进行赋值操作或被初始化,否则会编译出错
局部变量的有效范围:
- 局部变量的有效范围从该变量的声明开始到该变量的结束为止
- 在相互不嵌套的作用域可以同时声明同以个名称,类型相同的局部变量
- 在相互嵌套的区域中不可以声明同一个名称,编译器会报错
注意:在变量有效范围外,调用变量是一个常见的错误
3.2.6 静态(static)变量、常量和方法
(basic.faceobj.Staticemo)
由 static 修饰的变量、常量和方法被称为静态变量、常量和方法
如:在处理圆类或球类对象,两个类需要在同一个内存区共享一个数据 PI 常量,可以将该常量设置为静态的
一、statuc特点:
- static是一个修饰符,用于修饰成员
- static修饰的成员被所有的对象所共享
- static优先于对象存在,因为static的成员随着类的加载就已经存在了
- static修饰的成员多了一种调用方式,就可以直接被类名所调用。格式:
类名.静态成员
- static修饰的数据是共享数据,对象中的存储的是特有数据
二、成员变量和静态变量的区别?
不同点 | 静态变量 | 成员变量 |
---|---|---|
生命周期 | 随着类的加载而存在, 随着类的消失而消失 | 随着对象的创建而存在, 随着对象的被回收而释放 |
调用方式 | 可以被对象调用, 还可以被类名调用 | 只能被对象调用 |
别名 | 类变量 | 实例变量 |
数据存储位置 | 存储在方法区(共享数据区)的静态区, 所以也叫对象的共享数据 | 存储在堆内存的对象中, 所以也叫对象的特有数据 |
三、静态什么时候用?
1.静态变量
当分析对象中所具备的成员变量的值都是相同的(不需要修改,只需要使用),这个成员就可以被静态修饰
只要数据在对象中都是不同的,就是对象的特有数据,必须存储在对象中,是非静态的
2.静态函数
函数是否用静态修饰,就参考一点,就是该函数功能是否有访问到对象中的特有数据
简单点说,从源代码看,该功能是否需要访问非静态的成员变量,如果需要,该功能应该是非静态的
如果不需要,就可以将该功能定义成静态的。当然,也可以定义成非静态,但是非静态需要被对象调用,而仅创建对象调用非静态的,没有访问特有数据的方法,该对象的创建是没有意义
**四、静态使用的注意事项: **
- 静态方法只能访问静态成员. (非静态既可以访问静态, 又可以访问非静态)
- 静态方法中不可以使用this或者super关键字.
五、静态代码:随着类的加载而执行。而且只执行一次。作用:用于给类进行初始化
六、构造代码块:可以给所有对象进行初始化的
七、封装工具类:定义封装static工具类方法, 方便外部数组直接调用函数执行相应功能的方法
八、static函数内存图解
static函数内存图解.png- 执行这个StaticDemo类, 在方法区的静态区中加载了这个类, 并且加载默认构造函数StaticDemo(){}
- 在方法区中有个静态区中, 加载main方法和里面的代码
- main进栈
- Person.method(), 找到Person, 加载到方法区中的静态区, 有Person的构造函数和他的方法, 方法区中静态区添加Person的method方法和country=CN
- Person.method()压栈, 执行后弹栈
- Person p压栈, 并在堆中开辟地址0x0056, 并默认初始化值name=null, age=20
- 执行new Person(java, 20); 进行赋值
- Person p = new Person(java, 20)弹栈
- p.show()压栈, 其中this指向0x0056地址的对象, 执行后弹栈
- main方法弹栈
3.2.7 this关键字
(basic.faceobj.ThisDemo)
当成员变量和局部变量重名,可以用关键字this来区分
this代表当前对象(this就是所在函数所属对象的引用)
简单说:哪个对象调用了this所在的函数,this就代表哪个对象,见CarDemo中的setter(setColor、setNum)方法,里面均使用this
this也可以用于在构造函数中调用其他构造函数
注意: 只能定义在构造函数的第一行, 因为初始化动作要先执行
3.2.8 final关键字
(basic.faceobj.FinalDemo)
特点:
- final是一个修饰符,可以修饰类,方法,变量
- final修饰的类不可以被继承
- final修饰的方法不可以被覆盖
- final修饰的变量是一个常量,只能赋值一次
为什么要用final修饰变量?
在程序中,固定不变的变量值,就可以定义final,并用大写来表达常量的名称
3.3 对象
Java是一门面向对象的程序设计语言, 对象是由类抽象出来的, 所有的问题都是通过对对象来处理, 对象可以操作类的属性和方法解决相应的问题
3.3.1 对象的创建(对象的引用)
通过new操作符来创建对象(前面的数组和Stirng也是对象), 实例化一个对象就会自动调用一次构造方法, 实质上这个过程就是创建对象的过程. 创建对象, 实质上是一个引用, 引用知识存放一个对象的内存地址, 并非存放一个对象, 严格来说引用和对象是不同的.
例子1:
Person p2 = new Person("小强", 10);
p2.speak();
p2对象就是一个对象的引用, 这个引用在内存中为对象分配了存储空间, 可以在构造方法中初始化成员变量. 每个对象都是相互独立的, 在内存中占据独立的内存地址, 当一个对象的声明周期结束时, Java虚拟机自带的垃圾回收机制会进行相应处理。流程说明:
- main压栈,p2压栈
- Person p2: 在堆中开辟地址0x0045, 创建一个对象并默认初始化name=null, age=0
- new Person(name, age), 压栈
- 小强赋值到(哪个对象调用, 就赋值给哪个对象)堆中地址0x0045的name, 10赋值到0x0045的age(这一步才是进行赋值)
- 构造函数弹栈, 即new Person(name, age)弹栈
- p2.speak()压栈
- 执行后弹栈
3.3.2 匿名对象
概念: 没有名字的对象
new Car(); // 匿名对象, 其实就是定义对象的简写格式
一、当对象对方法仅进行一次调用的时候, 就可以简化成匿名对象
Car c = new Car();
c.run(); # new Car().run(); (匿名对象)
二、匿名对象可以作为实际参数进行传递
new Car().num = 5;
new Car().color = "green";
new Car().run();
3.3.3 对象的比较
(basic.faceobj.CompareDemo)
在Java语言中有两种对象的比较方式, 分别为 ==
运算符和 equals()
方法, 这两种方式有着本质区别
public class Compare {
public static void main(String[] args) {
String c1 = new String("abc"); // 创建两个String型对象引用
String c2 = new String("abc");
String c3 = c1; // 将c1对象引用赋予c3
// 使用"=="运算符比较c2与c3
System.out.println("c2==c3的运算结果为:" + (c1 == c3)); // true
System.out.println("c2==c3的运算结果为:" + (c2 == c3)); // false
// 使用equals()方法比较c2与c3
System.out.println("c2.equals(c3)的运算结果为:" + (c2.equals(c3))); // true
}
}
-
==
: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==
比较的是内存地址)。 -
equals()
: 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过
==
比较这两个对象。 - 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
3.3.4 对象的销毁
每个对象都有声明周期, 当对象的生命周期结束时, 分配给该对象的内存地址将会被回收, 在Java中有一套完整的垃圾回收机制, 那么何种对象会被Java虚拟机视为垃圾:
- 对象引用超过其作用范围, 则这个对象被视为垃圾
- 将对象赋值为null
虽然Java的垃圾回收机制已经很完善,但垃圾回收器只能回收那些由new操作符创建的对象,如果不是通过new操作符在内存中获取一块内存区域,这种对象可能不被垃圾回收机制所识别。所以提供了一个 finalize(),不保证一定执行(如Java虚拟机面临内存损耗待尽的情形, 不会执行垃圾回收),由于垃圾回收不受认为控制,因此 Java 提供 System.gc() 强制启动垃圾回收器(如同打110报警一样, 再不回收我就报警了)
3.4 接口、封装、继承、多态和抽象类
Java语言只支持单重继承, 不支持多继承(即一个类只能有一个父类)
为了解决需要多继承了来解决问题, Java提供了接口类来实现类的多重继承功能
3.4.1 接口的定义
(basic.faceobj.ext.InterfaceDemo)
使用interface来定义一个接口, 接口定义与类的定义类似, 也是分为接口的声明和接口体, 接口体由变量定义和方法定义两部分组成
一、格式
[修饰符] interface 接口名 [extends 父接口列表] {
[public] [static] [final] 变量;
[public] [abstract] 方法;
}
- 修饰符(可选):指定接口的访问权限,可选值为public, 如果省略则使用默认的访问权限
- 接口名:指定接口的名称, 接口名必须是合法的Java标识符,一般情况下, 要求首字母大写
- extends父接口名列表(可选):指定要定义的接口继承于哪个父接口,当使用extends关键字时, 父接口为必选参数
- 方法:接口中的方法只有定义而没有被实现
二、接口中的成员修饰符是固定的
- 成员常量: public static final
- 成员函数: public abstract
- 发现接口中的成员都是public的
接口的出现将"多继承"通过另一种形式体现出来, 即"多实现"
三、接口由来:当抽象类中的方法都是抽象修饰符时,就可以用接口(interface,特殊的抽象类)来表示
四、对于接口当中常见的成员:而且这些成员都有固定的修饰符
- 全局常量:public static final
- 抽象方法:public abstract
由此得出结论,接口中的成员都是公共的权限
五、特点
- 接口是对外暴露的规则,程序的功能扩展
- 接口的出现降低耦合性
- 接口可以用来多实现
- 接口与接口之间可以有继承关系
- 类与类之间是继承关系,类与接口直接是实现关系。
- 接口不可以实例化。只能由实现了接口的子类并覆盖了接口中所有的抽象方法后,该子类才可以实例化。否则,这个子类就是一个抽象类。
- Java中不直接支持多继承,因为会出现调用的不确定性。所以java将多继承机制进行改良,在java中变成了多实现。一个类可以实现多个接口。接口的出现避免了单继承的局限性。
3.4.2 接口的实现
(basic.faceobj.ext.InterfaceDemo)
接口在定义后,就可以在类中实现该接口,在类中实现接口可以使用implements关键字。格式:
[修饰符] class <类名>[extends 父接口列表] interface [接口列表] { }
- 修饰符:[可选] 指定接口的访问权限, 可选值为public, abstract和final
- 类名:指定类的名称, 接口名必须是合法的Java标识符, 一般情况下, 要求首字母大写
- extends父类名:[可选] 指定要定义的类继承于哪个父类, 当使用extends关键字时, 父类名为必选参数
- implements接口列表:[可选] 指定实现哪些接口, 当使用implements关键字时必填, 多个接口名时, 用逗号隔开
在类中实现接口时, 方法名, 返回值类型, 参数的个数及类型必须与接口中的完全一致, 并且必须实现接口中所有方法. 在类的继承中, 只能做单重继承, 而实现接口时, 可以实现多个接口(多接口列表用逗号','隔开)
3.4.3 封装
封装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式
好处:
- 将变化隔离
- 便于使用
- 提高重用性
- 提高安全性
封装原则:
- 将不需要对外提供的内容都隐藏起来
- 把属性都隐藏, 提供公共方法对其访问
/**
* 人:属性: 年龄.行为: 说话.
* private:私有, 是一个权限修饰符. 用于修饰成员.
* 私有的内容只在本类中有效. 注意: 私有仅仅是封装的一种体现而已.
*/
class Person {
/** 私有*/
private int age;
/** setXxx getXxx */
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
void speak() {
System.out.println("age=" + age);
}
}
public class PersonDemo {
public static void main(String[] args) {
Person p = new Person();
p.setAge(20);
p.speak();
}
}
3.4.4 继承的实现
(basic.faceobj.ext.ExtendsDemo)
在Java,继承通过extends关键字来实现,也就是extends指明当前类是子类,并指明是从哪个类继承而来。
即在子类的声明中,通过使用extends关键字显式地指明其父类(其父类称为基类,子类称为派生类)
一、格式
[修饰符] class 子类名 extends 父类名 {
类体
}
- 修饰符(可选):指定接口的访问权限, 可选值为public, abstract和final
- 子类名:指定子类的名称, 接口名必须是合法的Java标识符, 一般情况下, 要求首字母大写
- extends父类名:指定要定义的类继承于哪个父类
二、好处:
- 提高了代码的复用性
- 让类与类之间产生了关系,给第三个特征多态提供了前提
三、Java中支持单继承.不直接支持多继承, 但对C++中的多继承机制进行改良
- 单继承:一个子类只能有一个直接父类
- 多继承:一个子类可以有多个直接父类(java中不允许,进行改良)
注意:java不直接支持多继承,因为多个父类中有相同成员,会产生调用不确定性,java是通过 多实现
的方式来体现
错误写法:class DemoB extends Demo, DemoA
四、Java支持多层(多重) 继承
C继承B,B继承A:就会出现继承体系。
当要使用一个继承体系时,注意:
- 查看该体系中的顶层类,了解该体系的基本功能。
- 创建体系中的最子类对象,完成功能的使用。
五、什么时候定义继承呢?
当类与类之间存在着所属关系的时候,就定义继承。xxx是yyy中的一种。
xxx extends yyy
所属关系:is a 关系
六、在子父类中, 成员的特点体现
- 成员变量
- 成员函数
- 构造函数
七、内存图解
- 栈、堆、方法区
- main进方法区的静态方法
- main压栈
- Zi z压栈,加载父类到方法区,Fu类的方法加载到非静态区中,子类也加载到非静态区中
- new Zi(),堆中开辟空间0x0045的对象初始化(省略默认初始化num=0)Zi num=5、Fu num=4(即,子类父类2个一起存)
- show()压栈,this->0x0045的子类对象,super->0x0045的父类对象
- 执行后弹栈
3.4.5 继承的重写(覆盖)
(basic.faceobj.ext.ExtendsDemo)
重写是指父子类之间的关系,当子类继承父类中所有可能被子类访问的成员方法时,如果子类的方法名与父类的方法名相同,那么子类就不能继承父类的方法。此时,称为子类的方法重写了父类的方法,重写体现了子类补充或者改变父类方法的能力。通过重写,可以使一个方法在不同的子类中表现出不同的行为。
当子父类中出现成员函数一模一样的情况,会运行子类的函数,这称为覆盖操作,这是函数在子父类中的特性
一、函数两个特性
- 重载:同一个类中,Overload
- 覆盖:子类中,覆盖也称为重写、覆写、Override
二、覆盖注意事项
- 子类方法覆盖父类方法时,子类权限必须要大于等于父类的权限(例如父类是HashMap,子类只能大于HashMap,用HashMap或Map,详细见里氏替换原则)
- 静态只能覆盖静态,或被静态覆盖
- 用
@Override
标记标出的方法都会导致编译器检查方法的签名与超类中同名方法的签名是否相同。如果不同,就会报错。建议存在覆盖方法最好使用@Override标记。
三、什么时候使用覆盖操作?
- 当对一个类进行子类的扩展时,子类需要保留父类的功能声明。
- 但是要定义子类中该功能的特有内容时,就使用覆盖操作完成。
3.4.6 使用super关键字
(basic.faceobj.ext.SuperDemo)
子类可以继承父类的非私有成员变量和成员方法(不是private关键字修饰的),作为自己的成员变量和成员方法,如果子类中声明了与父类中同名的成员变量和成员方法,并且方法的返回值及参数个数和类型也相同,那么子类重写(覆盖)了父类的方法,而当想要调用父类中的方法,可以使用super关键字
一、super关键字的两种用途
- 调用父类的构造方法。eg:
super([参数列表]);
- 在子类中想操作被隐藏的成员变量和被重写的成员方法
eg:super.成员变量名
、super.成员方法名([参数列表])
二、this与super的用法很相似
- this:代表一个本类对象的引用,当本类的成员和局部变量同名用this区分
- super:代表一个父类空间,当子父类的成员变量同名用super区分父类
3.4.7 什么是多态
(basic.faceobj.ext.DuoTaiDemo)
多态性是面向对象程序设计的重要部分,通常使用方法的重载(Overload)和重写(覆盖)(Override)
重写之所以具有多态性,是因为父类的方法在子类中被重写,子类和父类的方法名称相同,但完成的功能却不一样,所以说,重写也具有多态性
一、定义
某一事物的多种存在形式,简单说:就是一个对象对应着不同类型。
class 动物 {}
class 猫 extends 动物 {}
class 狗 extends 动物 {}
猫 x = new 猫();
动物 x = new 猫(); //一个对象, 两种形态.(动物和猫)
猫这类事物即具备者猫的形态,又具备着动物的形态。这就是对象的多态性。
二、多态在代码中的体现
父类或者接口的引用指向其子类的对象。向上转型和向下转型
// 自动类型提升, 猫对象提升了动物类型. 但是特有功能无法s访问。作用就是限制对特有功能的访问
// 专业讲: 向上转型, 将子类型隐藏, 就不用使用子类的特有方法
Animal1 a = new Cat1();
a.eat();//cat eat
//a.catchMouse(); //无法使用Cat特有方法
// 如果还想用具体动物猫的特有功能, 你可以将该对象进行向下转型。向下转型的目的是为了使用子类中的特有方法
Cat1 c = (Cat1) a; //向下转型
// 注意:对于转型,自始自终都是子类对象在做着类型的变化。
三、多态的好处: 提高了代码的扩展性,前期定义的代码可以使用后期的内容。
四、多态的弊端: 前期定义的内容不能使用(调用)后期子类的特有内容。
五、多态的前提:
- 必须有关系、继承、实现
- 要有覆盖
六、多态时, 成员的特点
成员变量
编译时 参考引用型变量所属的类中的是否有调用的成员变量。有则编译通过;没有则编译失败
运行时 参考引用型变量所属的类中的是否有调用的成员变量,并运行该所属类中的成员变量
总结 编译和运行都参考等号的左边。(例如A a=new B() 参考A)
成员函数(非静态)
编译时 参考引用型变量所属的类中的是否有调用的函数。有则编译通过;没有则编译失败
运行时 参考的是对象所属的类中是否有调用的函数
总结 编译看左边,运行看右边。(因为成员函数存在覆盖特性)
静态函数(static定义的方法)
编译时 参考引用型变量所属的类中的是否有调用的静态方法
运行时 参考引用型变量所属的类中的是否有调用的静态方法
总结 编译和运行都看左边
3.4.8 抽象类
(basic.faceobj.ext.AbstractDemo)
抽象类就是只声明方法的存在而不去具体实现它的类。抽象类不能被实例化
定义抽象类时,要在class关键字前加上abstract关键字
一、特点
- 方法只有声明没有实现时,该方法就是抽象方法,需要abstract修饰。
- 抽象方法必须定义在抽象类中,该类必须也被abstract修饰
- 抽象类不可以被实例化。因为调用抽象方法没有意义
- 抽象类必须有子类覆盖了所有的抽象方法后,该子类才可以实例化,否则这个子类还是抽象类
二、格式
abstract class 类名 {
// 在抽象类种声明抽象方法
abstract <方法返回值类型> 方法名(参数列表);
}
- 方法返回值类型:指定方法的返回值类型,若没有返回值,用void标识
- 方法名:指定抽象方法的名称,必须是合法的Java标识符
- 参数列表:[可选] 指定方法中所需的参数(多个参数用逗号分隔)
四、注意
抽象方法不能使用private、static、final关键字进行修饰,因为必须子类实现抽象类才有意义,而加上这些修饰符,与子类无关
五、问题
1.抽象类有构造函数吗?
有, 用于给子类对象进行初始化
2.抽象类可以不定义抽象方法吗?
可以,目的是不让该类创建对象。AWT的适配器对象就是这种类。
3.abstract关键字不可以和哪些关键字并存?
private、static、final。非abstract可以定义静态方法和变量,以及final变量。
4.抽象类和一般类的相同点
抽象类 和 一般类 都是用来描述事物的,都在内部定义了成员
5.抽象类和一般类的不同点
一般类 | 抽象类 |
---|---|
可以被实例化 | 不可以被实例化 |
有足够的信息描述信息 | 描述事物的信息可能不足 |
不能定义抽象方法,只能定义非抽象方法 | 可以定义抽象方法,同时也可以定义非抽象方法 |
6.抽象一定是个父类吗?
一定是,因为需要子类覆盖其方法才可以对子类实例化
3.4.9 抽象类和接口的异同点
(basic.faceobj.ext.InterfaceDemo, AbstractDemo)
相同点:都是不断向上抽取而来的,都可以定义静态方法,静态变量
不同点:
接口 | 抽象类 | |
---|---|---|
关系 | 接口的实现是 like a 关系 。在定义体系额外功能。从设计层面来说,接口是对行为的抽象,是一种行为的规范 |
抽象类的继承是 is a 关系 。在定义该体系的基本共性内容。从设计层面来说,抽象是对类的抽象,是一种模板设计 |
子类实现 | 接口需要被实现,可以多实现 | 抽象类需要被继承,只能单继承 |
常量 | 除了static、final变量,不能有其他变量 | 不限制 |
方法 | 默认是 public。所有方法在接口中不能有实现 (Java 8 开始接口方法可以有默认实现) | 可以有非抽象的方法。抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰) |
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错
JDK8 对于 接口 和 抽象类 改动:
- 关于接口
- JDK 1.8 以前,接口中的方法必须是public的
- JDK 1.8 时,接口中的方法可以是public的,也可以是default的
- JDK 1.9 时,接口中的方法可以是private的
- 关于抽象类
- JDK 1.8 以前,抽象类的方法默认访问权限为protected
- JDK 1.8 时,抽象类的方法默认访问权限变为default
3.5 内部类
如果在一个类中再定义一个类,就称为内部类。
内部类又如下分类:
- 成员内部类:成员内部类是最普通的内部类,它的定义为位于另一个类的内部
- 局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内
- 匿名内部类:匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。
- 静态内部类:又叫静态局部类、嵌套内部类,就是修饰为static的内部类。声明为static的内部类,不需要内部类对象和外部类对象之间的联系,就是说我们可以直接引用
outer.inner
,即不需要创建外部类,也不需要创建内部类。
内部类对比:
内部类 | 对比 |
---|---|
成员内部类 | 1).new OuterClass().new MemberInnerClass() 2).成员内部类可以访问外部非静态变量 |
局部内部类 | 局部内部类可以访问外部非静态变量 |
匿名内部类 | new OuterClass.StaticInnerClass() |
静态内部类 | 1).静态内部类 的非静态方法 无法访问 外部非静态变量 2).静态内部类 的静态方法 无法访问 内部非静态变量 和 外部非静态变量 |
(basic.faceobj.InnerClass)
public class InnerClassDemo {
public static void main(String[] args) {
OuterClass outerObj = new OuterClass();
OuterClass.MemberInnerClass memberInnerObj = outerObj.new MemberInnerClass();
System.out.println("\n--> 成员内部类");
memberInnerObj.print();
System.out.println("\n--> 局部内部类");
System.out.println(outerObj.getNum(1, 2));
System.out.println("\n--> 静态内部类");
OuterClass.StaticInnerClass.printStatic();
OuterClass.StaticInnerClass staticInnerClass = new OuterClass.StaticInnerClass();
staticInnerClass.print();
System.out.println("\n--> 匿名内部类");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类的 Runnable");
}
}).start();
}
}
class OuterClass {
private static int outerStaticVal = 100;
private int outerVal = 100;
/** 成员内部类:成员内部类是最普通的内部类,它的定义为位于另一个类的内部 */
public class MemberInnerClass {
/** 无法定义静态变量 */
// private static int innerStaticVal = 10;
private int innerVal = 10;
public void print() {
System.out.println("outerStaticVal:" + outerStaticVal);
System.out.println("outerVal:" + outerVal);
System.out.println("innerVal:" + innerVal);
}
}
/** 局部内部类:*/
public int getNum(int num1, int num2) {
class ParkInnerClass {
int num1;
int num2;
public ParkInnerClass(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
public int calSum() {
return num1 + num2;
}
}
ParkInnerClass parkInnerClass = new ParkInnerClass(num1, num2);
return parkInnerClass.calSum();
}
/** 静态内部类 */
public static class StaticInnerClass {
private static int innerStaticVal = 20;
private int innerVal = 20;
public void print() {
System.out.println("outerStaticVal:" + outerStaticVal);
// 静态内部类 的非静态方法 无法访问 外部非静态变量
// System.out.println("outerVal:" + outerVal);
System.out.println("innerStaticVal:" + innerStaticVal);
System.out.println("innerVal:" + innerVal);
}
public static void printStatic() {
System.out.println("outerStaticVal:" + outerStaticVal);
System.out.println("innerStaticVal:" + innerStaticVal);
// 静态内部类 的静态方法 无法访问 内部非静态变量 和 外部非静态变量
// System.out.println("innerVal:" + innerVal);
}
}
/** 静态类也可以继承 */
public class ExtInnerClass extends MemberInnerClass {
@Override
public void print() {
System.out.println("--> ExtInnerClass print()");
super.print();
}
}
public static class ExtStaticInnerClass extends StaticInnerClass {
@Override
public void print() {
System.out.println("--> ExtStaticInnerClass print()");
super.print();
}
}
}
3.5.1 匿名内部类常用场景
常用场景:函数参数是接口类型时,且接口中的方法不超过3个,可以用匿名内部类作为实际参数进行传递
必须有前提:内部类必须继承或者实现一个外部类或者接口。
匿名内部类:其实就是一个匿名子类对象。
编译说明:匿名内部类编译成以 外部类名$序号
为名称的 .class
文件,序号以1n排列(代表1n个匿名内部类)
格式: new 父类or接口() { 子类内容 }
return new A() {
// 内部类体
};
A表示对象名,匿名内部类没有名称(使用默认构造方法来生成匿名内部类的对象)。在匿名内部定义结束后,需要加分号表示,这个分号并不代表定义内部类结束的表示,而代表创建匿名内部类的应用表达式的标识
3.6 枚举类
(basic.faceobj.enums包下的例子)
枚举类型是一种特殊形式的类,定义枚举类型时,设定的枚举常量以一个类的实例被创建,这个类以 java.lang
包中的 Enum 类作为超类。每个枚举常量对应的对象将常量的名称存储在一个域中,而枚举类类型从Enum类中继承toString()方法。
Enum类中的toString()方法返回枚举常量的原始名称,所以使用println()方法输出枚举常量时会获得枚举常量的名称。
表示枚举常量的对象也存储一个整数域。枚举中的每个常量默认地都被赋予一个与其他常量不同的整数值。这些值按照设定它们时的顺序被赋给枚举常量,第一个常量从零开始,然后第二个常量是一,后面依此类推。通过调用常量的 ordinal()
方法就能获得它的整数值,但是一般来说不应该需要这样做.
- 比较:可以使用 equals() 方法来比较枚举类型的整数值是否相等。
- 构造函数:需要注意的是枚举常量列表以分号结束,列表中的每个常量在括号中都有对应的属性,而且这些值会被传递给添加到类中的构造函数(每个枚举常量的出现都会导致对枚举默认构造函数的一次调用)。
- 顺序:即使已经添加了自己的构造函数,从基类Enum中继承的用来存储常量名称及其整数值的域仍然会被正确地设置。compareTo() 方法实现的常量顺序仍然由定义中常量出现的顺序决定。
- 修饰符:注意决不能将枚举类中的构造函数声明为public。如果这样做,enum类定义就不会被编译。惟一允许对定义为枚举的类的构造函数应用的修饰符是private,这导致只有在类的内部调用构造函数。
- Switch语句:注意在switch语句中使用this作为控制表达式。这是因为this引用当前的实例,它也是一个枚举常量。因为控制表达式是一个枚举常量,所以case标签是常量名称,它们不需要由枚举名称进行限定。
网友评论