打怪升级之小白的大数据之旅(十三)
Java基础语法之面向对象的三大特性–多态
上次回顾:
上一章,对面向对象三大特性之一的继承进行介绍,今天是三大特性的最后一章–多态,大家还记得前面讲的知识点么?封装–主要用于对类的封装,继承–主要用于子类对父类成员属性/方法的传承,多态,我将它理解是多种形态,下面,即将开始我的表演…
面向对象三大特性之多态
多态是Java面向对象中的核心,怎么理解多态呢?举个栗子:我有一杯水,在100度下是液态的水,在100度以上就是气态,在0度以下就是固态.所以,我对多态的理解就是四个字: 多种形态
上面这张图我再来回顾一下面向对象的三大特性:
- 猫类,狗类,动物类,单独一个可以理解为封装,比如猫类的封装,里面封装了猫的属性,猫的行为等, 这个就是封装
- 猫和狗都是动物,他们都有共同的行为—吃,因此,它们都继承了动物类吃的方法, 这个就是继承
- 虽然猫和狗都继承了动物吃的方法,但它们各自有自己喜欢的食物,猫吃猫粮和鱼,狗吃狗粮和骨头,猫和狗都继承了动物的行为,但它们各自有属于自己的行为,因此,猫类和狗类,重写了动物类的方法,这个就是多态
多态概述
-
Java中,多态就是父类的引用指向子类的对象
理解Java中的多态前,先写一个示例代码:// 测试入口类 public class Test1 { public static void main(String[] args) { Person p=new Person(); //测试人喂狗吃饭 p.feed(new Dog()); } } //定义一个Animal类 public class Animal { //String name = "animal"; public void eat(){ System.out.println("动物吃饭"); } } //定义一个Dog类 public class Dog extends Animal {//继承Animal类 //String name = "dog"; @Override public void eat() { System.out.println("狗啃骨头"); } } //定义一个Person类 public class Person { //喂狗吃饭 public void feed(Dog dog){ dog.eat(); } }
-
在上述代码中,我只有一只狗,假设我想再买一只猫,那么我该怎么做?
-
很简单,我首先创建一个Cat猫类,然后在Person类中增加喂猫的方法,那如果后期还要不断的增加新功能(新增各种动物类),就要不断增加Person类对应的方法。
-
这里就出现了一个问题:违背了软件开发中的开闭原则(对修改关闭,对扩展开放),并且大量类同代码冗余,即每次新增的方法除了参数类型不同,其他都基本相同。
-
针对上述的问题,就需要多态出场了,下面是利用多态改造的代码:
//修改后的Person类 public class Person { //喂动物吃饭 public void feed(Animal animal){ animal.eat(); } }
是不是很简单?不信测试一下,后面不管需要添加多少个类,我只需要创建新的类,然后调用Person的feed方法,里面传入新的动物就好了:
//测试人喂猫吃饭
p.feed(new Cat());
// 测试人喂兔子吃饭
p.feed(new Rabbit());
语法格式
是不是感受到多态的魅力了?下面开始正式理解与学习多态的用法
- 语法格式:
父类类型 变量名 = 子类对象; Ainmal dog = new Dog();
- 理解: 在Java的编译期,dog是个动物,当它运行后,dog就是只狗,new Dog(), 也就是创建了一个Dog的狗类对象dog
- 示例代码:
public class Test2 { public static void main(String[] args) { Animal animal=new Animal(); animal.eat(); Dog dog=new Dog(); dog.eat(); System.out.println("----------------"); //多态写法:父类的引用指向子类的对象。 Animal dog2=new Dog();//狗也是一个动物 dog2.eat();//运行时动态的调用了Dog的eat方法 Animal cat=new Cat();//猫也是一个动物 cat.eat();//运行时调用Cat的eat方法 } }
多态的理解
- 引用类型变量在编译期呈现左边父类的行为特征,运行时呈现右边创建的不同子类对象的特征
- 通俗点来说就是: 编译看左边,运行看右边
- 假设我在子类建立一个catchMouse方法,但是父类没有该方法就会造成编译错误
- 示例代码:
public class Test { public static void main(String[] args) { // 多态形式,创建对象 Animal cat = new Cat(); // 调用的是 Cat 的 eat cat.eat(); //a1.catchMouse();//错误,编译看左边,catchMouse()是子类特有的方法,左边父类中没有,编译失败 // 多态形式,创建对象 Animal dog = new Dog(); // 调用的是 Dog 的 eat dog.eat(); } } // 创建猫类,继承自动物类 class Cat extends Animal{ public void eat(){ System.out.println("动物吃饭"); } // 子类特有的方法 public void catchMouse(){ System.out.println("猫抓老鼠~") } }
这个编译的错误解决方案,在下面再多态的类型转换会讲到~
多态的前提
- 有继承关系
- 有方法重写
- 如果没有方法重写,运行时调用子类的方法,实际还是调用的从父类继承的方法,不具有多态性
- 就像我开头提到的,猫类和狗类都继承了动物类吃的方法,并且各自有自己独特吃的方法,因此重写后,就具有了多态性
多态的好处
- 正如前面案例所演示的那样,多态的好处就是:
- 多态可以降低类与类之间的耦合度
- 提高程序的扩展性
多态的应用
既然知道了多态的好处和语法格式,那么它的使用场景呢?
-
多态应用在形参实参中
父类类型作为方法中的形参,子类对象作为方法的实参public class Test01 { public static void main(String[] args) { showAnimalEat(new Dog()); //形参 Animal a,实参new Dog() //实参给形参赋值 Animal a = new Dog() 多态引用 showAnimalEat(new Cat());//形参 Animal a,实参new Cat() //实参给形参赋值 Animal a = new Cat() 多态引用 } /* * 设计一个方法,可以查看所有动物的吃的行为 * 关注的是所有动物的共同特征:eat() * 所以形参,设计为父类的类型 * 此时不关注子类特有的方法 */ public static void showAnimalEat (Animal a){ a.eat(); // a.catchMouse();//错误,因为a现在编译时类型是Animal,只能看到父类中有的方法 } }
-
多态应用在数组中
数组元素类型声明为父类类型,实际存储的是子类对象public class Test02 { public static void main(String[] args) { /* * 声明一个数组,可以装各种动物的对象,看它们吃东西的样子 */ Animal[] arr = new Animal[2]; //此时不是new Animal的对象,而是new Animal[]的数组对象 //在堆中开辟了长度为5的数组空间,用来装Animal或它子类对象的地址 arr[0] = new Cat();//多态引用 左边arr[0] 是Animal类型,右边是new Cat() //把Cat对象,赋值给Animal类型的变量 arr[1] = new Dog(); for (int i = 0; i < arr.length; i++) { arr[i].eat(); //arr[i].catchMouse();错误,因为arr[i]现在编译时类型是Animal,只能看到父类中有的方法 } } }
-
多态应用在返回值
方法的返回值类型声明为父类类型,实际返回值是子类对象public class Test03 { public static void main(String[] args) { Animal c = buy("猫咪"); System.out.println(c.getClass()); c.eat(); } /* * 设计一个方法,可以购买各种动物的对象,此时不确定是那种具体的动物 * * 返回值类型是父类的对象 * * 多态体现在 返回值类型 Animal ,实际返回的对象是子类的new Cat(),或new Dog() */ public static Animal buy(String name){ if("猫咪".equals(name)){ return new Cat(); }else if("小狗".equals(name)){ return new Dog(); } return null; } }
多态的向上转型与向下转型
-
一个对象在new的时候创建的是哪个类型的对象,它从头至尾都不会改变,即这个对象的运行时类型,本质的类型不会变,这个和基本数据类型的转换是不同的,不要混淆了哈
-
但是将这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同
class Animal { void eat(){ System.out.println("~~~"); } } class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void catchMouse() { System.out.println("抓老鼠"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } public void watchHouse() { System.out.println("看家"); } } class Test{ public static void main(String[] args){ Cat a = new Cat();//a编译时类型是Cat Animal b = a;//b编译时类型是Animal Object c = a;//c编译时类型是Object //运行时类型 System.out.println(a.getClass()); System.out.println(b.getClass()); System.out.println(c.getClass()); //以上输出都一样,都是Cat类型 //a,b,c的编译时类型不同 //通过a能调用Cat中所有方法,包括从父类继承的,包括自己扩展的 //通过b只能调用Animal类及它的父类有的方法,不能调用Cat扩展的方法 //通过c只能调用Object类才有的方法 } }
-
为什么要进行类型转换?就像上述的案例b,它只能调用父类继承下来的方法,却无法调用自己的独特方法,我在前面猫抓老鼠那个案例里也有提到过,此时把前面遗留的坑填了(我又挖了个坑~)
-
这个编译错误的原因就是因为多态,它一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现这个现象
-
使用父类变量接收了子类对象后,我们就不能调用子类特有的方法了吗?可以,就是接来下的多态的向上转型与向下转型
☛向上转型: 当左边的变量的类型(父类) > 右边对象/变量的类型(子类)时,我们就称为向上转型- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法
- 在运行时,仍然是对象本身的类型
- 此时一定是安全的,并且是自动完成的
- 假设需要调用子类特有的变量/方法,就会造成编译错误
☛向下转型: 当左边的变量类型(子类)<右边对象/变量的类型(父类),我们就称之为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 在运行时,仍然是对象本身
- 此时,不一定是安全的,需要使用(类型)进行强制转换
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCaseException,为了安全,我们可以通过isinstanceof关键字进行判断
-
下面使用示例代码来解决编译错误的问题:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse // 向下转型 //Dog d = (Dog)a; //这段代码可以通过编译,但是运行时,却报出了ClassCastException //这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系, //不符合类型转换的定义。 //d.watchHouse(); // 调用的是 Dog 的 watchHouse Animal a2 = new Animal(); // Dog d2 = (Dog)a2;//这段代码可以通过编译,但是运行时,却报出了ClassCastException // d2.watchHouse(); // 调用的是 Dog 的 watchHouse } }
-
就像上面这个示例代码中,虽然解决了编译错误,但向下转型是不安全的,狗并不是猫,无法调用猫类抓老鼠的方法
-
有问题就要想办法解决,于是为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常
-
语法格式:
变量名/对象 instanceof 数据类型
-
示例代码:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 if (a instanceof Cat){ Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse } else if (a instanceof Dog){ Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse } } }
-
instanceof语法说明
- 对象/变量的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
- 对象/变量的运行时类型<= instanceof后面数据类型,才为true
多态引用时关于成员变量与成员方法引用的原则
- 这个内容理解就好,只是为了让大家更好的理解多态
- 在讲解引用原则前,我先普及一下虚方法和非虚方法,方便下面的案例演示与理解
- 虚方法: 可以被重写方法
- 非虚方法: 不可以被重写的方法,例如:私有方法,静态方法,final修饰的方法等
- 成员变量: 只看编译时类型
如果直接访问成员变量,那么只看编译时类型/* * 成员变量没有重写,只看编译时类型 */ public class TestExtends { public static void main(String[] args) { Son s = new Son(); System.out.println(s.a);//2,因为son的编译时类型是Son System.out.println(((Father)s).a);//1 ((Father)son)编译时类型,就是Father Father s2 = new Son(); System.out.println(s2.a);//1 son2的编译时类型是Father System.out.println(((Son)s2).a);//2 ((Son)son2)编译时类型,就是Son } } class Father{ int a = 1; } class Son extends Father{ int a = 2; }
- 非虚方法: 只看编译时类型
编译期就可以明确要调用的方法
前面我提到了非虚方法,下面具体讲一下:
在Java中的非虚方法有三种:- static修饰的静态方法,这种方法在编译时确定在运行时不会改变
- 同样确定在运行不会改变的方法,这些方法包括:私有方法、实例构造方法
- 由final关键字修饰的方法,final这个关键字等下会详细介绍,就先记住,由它修饰的方法就是非虚方法
示例代码:
注意: 静态方法不能被重写,调用静态方法最好实用类名.静态方法public class Test { public static void main(String[] args) { Father f = new Son(); f.test();//只看编译时类型 f.method(); } } class Father{ public static void test(){ System.out.println("Father.test"); } public void method(){ System.out.println("Father.method"); fun();//看运行时类型 other();//看编译时类型 } public void fun(){ System.out.println("Father.fun"); } private void other(){ System.out.println("Father.other"); } } class Son extends Father{ public static void test(){ System.out.println("son"); } public void fun(){ System.out.println("Son.fun"); } private void other(){ System.out.println("Son.other"); } }
介绍完非虚方法,下面介绍一下虚方法,正如前面提到的,虚方法就是可以被重写的方法,它是判断是不是多态的前提
-
虚方法: 静态分派与动态绑定
- 在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法
- 当我们通过对象方法的形式调用一个方法,需要用到静态分派和动态绑定来确定它具体的执行位置
- 静态分派: 首先看对象编译时的类型,在这个对象编译时类型中找到最匹配的方法,也就是实参编译时类型与形参的类型最匹配的方法
- 动态绑定: 当静态分派确定之后,再根据对象运行时的类型,如果这个对象的运行时类重写了刚刚静态分派确定的方法,那么就执行重写的,否则仍然执行静态分派确定的方法
- 这里有个小技巧: 表达式左边就是编译,右边就是运行
-
示例代码一:没有重载,有重写
abstract class Animal { public abstract void eat(); } class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } } public class Test{ public static void main(String[] args){ Animal a = new Cat(); a.eat(); } } /* 如上代码在编译期间先进行静态分派:此时a的编译时类型是Animal类,所以去Animal类中搜索eat()方法,如果Animal类或它的父类中没有这个方法,将会报错。 而在运行期间动态的在进行动态绑定:a的运行时类型是Cat类,而子类重写了eat()方法,所以执行的是Cat类的eat方法。如果没有重写,那么还是执行Animal类在的eat()方法 */
-
示例代码二:有重载,没有重写
class MyClass{ public void method(Father f) { System.out.println("father"); } public void method(Son s) { System.out.println("son"); } public void method(Daughter f) { System.out.println("daughter"); } } class Father{ } class Son extends Father{ } class Daughter extends Father{ } public class TestOverload { public static void main(String[] args) { Father f = new Father(); Father s = new Son(); Father d = new Daughter(); MyClass my = new MyClass(); my.method(f);//father my.method(s);//father my.method(d);//father } } /* 如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。 而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。 */
大家可能会疑问,不是应该分别执行method(Father f)、method(Son s)、method(Daughter d)吗?
因为此时实参f,s,d编译时类型都是Father类型,因此method(Father f)是最合适的
-
示例代码三: 有重载,没有重写
class MyClass{ public void method(Father f) { System.out.println("father"); } public void method(Son s) { System.out.println("son"); } } class Father{ } class Son extends Father{ } class Daughter extends Father{ } public class TestOverload { public static void main(String[] args) { MyClass my = new MyClass(); Father f = new Father(); Son s = new Son(); Daughter d = new Daughter(); my.method(f);//father my.method(s);//son my.method(d);//father } } /* 如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。 而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型 */
大家可能会疑问,这次为什么分别执行method(Father f)、method(Son s)?
因为此时实参f,s,d编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
-
示例代码四: 有重载,没有重写
class MyClass{ public void method(Father f) { System.out.println("father"); } public void method(Son s) { System.out.println("son"); } } class MySub extends MyClass{ public void method(Daughter d) { System.out.println("daughter"); } } class Father{ } class Son extends Father{ } class Daughter extends Father{ } public class TestOverload { public static void main(String[] args) { MyClass my = new MySub(); Father f = new Father(); Son s = new Son(); Daughter d = new Daughter(); my.method(f);//father my.method(s);//son my.method(d);//father } } /* 如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。 而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型 */
大家可能会疑问,my对象不是MySub类型吗,而MySub类型中有method(Daughter d)方法,那么my.method(d)语句应该执行MySub类型中的method(Daughter d)方法?
- my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法,
- f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
- 而在MySub类中并没有重写method(Father f)方法,所以仍然执行MyClass类中的method(Father f)方法
-
示例代码五: 有重载,有重写
class MyClass{ public void method(Father f) { System.out.println("father"); } public void method(Son s) { System.out.println("son"); } } class MySub extends MyClass{ public void method(Father d) { System.out.println("sub--"); } public void method(Daughter d) { System.out.println("daughter"); } } class Father{ } class Son extends Father{ } class Daughter extends Father{ } public class TestOverloadOverride { public static void main(String[] args) { MyClass my = new MySub(); Father f = new Father(); Son s = new Son(); Daughter d = new Daughter(); my.method(f);//sub-- my.method(s);//son my.method(d);//sub-- } } /* 如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。 而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型 */
大家可能会疑问,my对象不是MySub类型吗,而MySub类型中有method(Daughter d)方法,那么my.method(d)语句应该执行MySub类型中的method(Daughter d)方法?
- my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法,
- f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
- 而在MySub类中重写method(Father f)方法,所以执行MySub类中的method(Father f)方法
final关键字
我又填坑啦~下面介绍在非虚方法中遗留的坑–final关键字
- final关键字,最终的,不可更改的
final应用场景:
- 修饰类
final修饰类,表示这个类不能被继承,没有子类final class Eunuch{//太监类 } class Son extends Eunuch{//错误 }
就像古代的太监,他是没有后代的…
- 修饰方法
表示这个类不能被子类重写class Father{ public final void method(){ System.out.println("father"); } } class Son extends Father{ public void method(){//错误 System.out.println("son"); } }
就是因为这个,它才会被归类为非虚方法
-
声明常量
final修饰某个变量(成员变量或局部变量),表示它的值就不能被修改,即常量,常量名建议使用大写字母如果某个成员变量用final修饰后,没有set方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值
public class Test{ public static void main(String[] args){ final int MIN_SCORE = 0; final int MAX_SCORE = 100; } } class Chinese{ public static final String COUNTRY = "中华人民共和国"; private String name; public Chinese( String name) { super(); this.name = name; } public Chinese() { super(); } public String getName() { return name; } public void setName(String name) { this.name = name; } //final修饰的没有set方法 public static String getCountry() { return COUNTRY; } }
native关键字
这个关键字的作用就是声明这个方法可以在JVM内存的本地方法栈中开辟一个空间
还记得这张图吗?我又把它拿出来,前面我们分别介绍了虚拟机栈,堆,方法区,这个native就是当程序中调用了native的本地方法时,本地方法执行期间的内存区域
了解一下就好,需要详细了解的话…度娘,这个坑我就不填了.
Object根父类
概述
Java同python一样,都是面向对象语言,它们的祖宗都是Object,也就是所有类的父类,每个类都直接或间接继承Object类
- Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用
- 所有对象(包括数组)都实现这个类的方法。
- 如果一个类没有特别指定父类,那么默认则继承自Object类,如:
public class MyClass /*extends Object*/ { // ... }
Object的API
- 首先再普及一下API这个东东,API全名:Application Programming Interface,它是应用程序编程接口,
- Java API是一本程序员的字典 ,是JDK中提供给我们使用的类的说明文档。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。
- 在API文档中是无法得知这些类具体是如何实现的,如果要查看具体实现代码,那么我们需要查看src源码
- 下面我来介绍几个后面会用到的几个API,具体的可以看文档
-
hashCode()
1). int hashCode()返回该对象的哈希码值
2). 默认情况下,此哈希值是此对象的内存地址转换而来的整数
3). 如果两个对象的hashCode不同,那么,它们一定不是同一个对象
4). 一般建议重写该方法,用于根据对象的内容来区分是否是同一个对象public static void main(String[] args) { System.out.println("Aa".hashCode());//2112 System.out.println("BB".hashCode());//2112 }
-
getClass()
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法public static void main(String[] args) { Object obj = new String(); System.out.println(obj.getClass().getName());//运行时类型 } obj.getClass().getName()与直接打印对象System.out.println(obj)的值相同
-
toString()
1). 默认情况下,toString()返回的是“对象的运行时类型名称 @ 对象的hashCode值的十六进制形式"
2). 通常是建议重写
3). 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
4). 因为Java的引用数据类型的变量中存储的实际上是对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
5). 这个方法通常重写的方式:// 自定义的类 public class Person { private String name; private int age; @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } // 省略构造器与Getter Setter }
-
equals()
用于判断当前对象this与指定对象obj是否“相等”
1). 默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值
2). 我们一般选择重写,重写有些要求:
-
如果重写equals,那么一定要一起重写hashCode()方法,因为规定:
-
如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
-
如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
-
如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
-
-
如果重写equals,那么一定要遵循如下几个原则:
-
自反性:x.equals(x)返回true
-
传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true
-
一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致
-
对称性:x.equals(y)与y.equals(x)结果应该一样
-
非空对象与null的equals一定是false
-
-
示例代码:
class User{ private String host; private String username; private String password; public User(String host, String username, String password) { super(); this.host = host; this.username = username; this.password = password; } public User() { super(); } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User [host=" + host + ", username=" + username + ", password=" + password + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((host == null) ? 0 : host.hashCode()); result = prime * result + ((password == null) ? 0 : password.hashCode()); result = prime * result + ((username == null) ? 0 : username.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; User other = (User) obj; if (host == null) { if (other.host != null) return false; } else if (!host.equals(other.host)) return false; if (password == null) { if (other.password != null) return false; } else if (!password.equals(other.password)) return false; if (username == null) { if (other.username != null) return false; } else if (!username.equals(other.username)) return false; return true; } }
- finalize()
这个方法用于最终清理内存
介绍这个方法,主要是有很多面试题会问到
示例代码:ublic class TestFinalize { public static void main(String[] args) { for (int i = 0; i < 10; i++) { MyData my = new MyData(); } System.gc();//通知垃圾回收器来回收垃圾 try { Thread.sleep(2000);//等待2秒再结束main,为了看效果 } catch (InterruptedException e) { e.printStackTrace(); } } } class MyData{ @Override protected void finalize() throws Throwable { System.out.println("轻轻的我走了..."); } }
面试题:对finalize()的理解?再说说finalize与final的联系
- 当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法,不是由程序员手动调用。
- 这个方法与C语言的析构函数不同,C语言的析构函数被调用,那么对象一定被销毁,内存被回收,而finalize方法的调用不一定会销毁当前对象,因为可能在finalize()中出现了让当前对象“复活”的代码
- 每一个对象的finalize方法只会被调用一次。
- 子类可以选择重写,一般用于彻底释放一些资源对象,而且这些资源对象往往是通过C/C++等代码申请的资源内存
- final与finalize的联系就是它们长得很像,这两个是不同的东西,代表共同的功能.除了长得像,再没有任何关系
总结
本章对面向对象的三大特性就讲解完毕啦,是不是感觉内容很多,别慌,跟着这章的开篇那样理解就好,剩下的就是多写代码,我会在下一章专门整理一份这段时间学习的练习题集,让大家巩固一下所学的东西,消化吸收一下,
之后,我会开始介绍面向对象基础的最终章,主要是抽象类、接口、内部类等。再次感谢大家观看,欢迎后台留言吐槽。。