第八节:面向对象高级
记录现阶段感觉有用的知识点,查缺补漏。
继承
-
关键字 extends。
-
Java 里的继承是 单继承。 一个子类 只能继承自 一个父类。
-
子类的内存结构:
类 都是在 堆 上分配内存空间的。 (创建子类对象时,都要先去看看有没有父类)
只不过子类里有个 super( ) 的关键字,指向了父类的内存地址。
也就是说,实现继承的手段,就是在子类的内存空间里,保存了父类的信息(内存地址)。
重写(Override)
-
里氏替换原则:
通俗点说:子类不能改变父类原有的功能,但是可以扩展父类的功能。
1987年提出来的:
Inheritance should ensure that
any property proved about supertype objects also holds for subtype objects.
-
重写需要满足:(可以用 @Override 注解让 编译器来检查)
- 子类方法 的访问权限 不能小于 父类方法 的访问权限。(可以访问子类方法,就一定能访问父类方法)
- 子类方法 的返回值类型 是 父类方法 的返回类值类型或其子类型。
- 子类方法 抛出的异常 是 父类方法抛出的异常或其子异常。
-
方法的调用顺序:
先去 当前类看有没有相应方法,再去父类看有没有继承过来的相应方法。
如果还不行,就对参数进行类型转换,然后还是先看当前类,再看父类。
public class OverrideTest2 { public static void main(String[] args) { A a = new A(); B b = new B(); C c = new C(); D d = new D(); // 当前就有的方法。 a.show(a); // A.show(A) a.show(c); // A.show(C) // b的父类A中有相应方法。 b.show(c); // A.show(C) // 将b进行类型转换为其父类A。 a.show(b); // A.show(A) // 将d进行类型转换为其父类C。 // b的父类A中有相应的方法。 b.show(d); // A.show(C) // 引用的还是 B对象, 和B对象调用的情况是一样的。 A ba = new B(); ba.show(c); // A.show(C) ba.show(d); // A.show(C) ba.show(a); // B.show(A) } } // 继承关系: A <--- B <--- C <--- D class A{ public void show(A obj) { System.out.println("A.show(A)"); } public void show(C obj) { System.out.println("A.show(C)"); } } class B extends A { @Override // 重写了 A 类的方法。 public void show(A obj) { System.out.println("B.show(A)"); } } class C extends B { } class D extends C { }
-
存在继承的情况下,代码块的初始化顺序:
父类(静态变量、静态代码块)
子类(静态变量、静态代码块)
父类(实例变量、构造代码块)
父类(构造函数)
子类(实例变量、构造代码块)
子类(构造函数)
虽然重写(Override)和重载(Overload)听着很像,但是搞明白他们代表什么,还是很好区分的。。。
final 关键字
-
被 final 修饰过的变量,就是常量。
对于基本类型, final 使其数值不能改变。
对于引用类型, final 使其引用不能改变。但是引用指向的 对象里的数据是可以改变的。
-
public static final :
就是他的名称一样: 全局的(不同包下可访问),静态的(类名可访问),常量(不可变)。
-
final 修饰的方法: 不能被子类重写。
-
final 修饰的类: 不能被继承。
抽象类
有时候我们描述事物的时候,不是把他定死的,也是一个很抽象的事物。
我们就可以用 抽象类 来表示这个事物。
-
包含 抽象方法的类,都是抽象类。 用 abstract 关键字 进行声明。
-
抽象方法没有实现体,只有声明,等着子类继承从而实现他。
子类要实现 父类的所有抽象方法,否则子类仍然是 抽象类。
-
可以有构造方法,但我们不能进行 new 实例化。继承的子类实例化时,JVM会用他的父类抽象类的构造方法
-
抽象类因为要有 子类继承,才有存在意义。所以权限修饰符只能用 public(默认)、protected。
接口
接口是一种规范,是一种更加纯粹的抽象。接口里面只能有 全局常量、和抽象方法。
-
Java8 之前,接口不用有任何方法的实现; Java8 开始,接口里可以有默认方法的实现。
-
用接口, 需要用 implements 关键字来实现。 可以有多个实现。
实现接口的类,仍然要实现接口里所有没有实现的抽象方法。否则是抽象类。
-
接口的字段,默认的就是 public static final;
接口的方法,默认的就是 public abstract。
-
接口之间是允许 多继承的。(继承的还是接口,不要去继承类去了。。。)
就算继承的多个接口中相同的抽象方法也没关系,最后在 “该接口” 中,对该方法只有一种实现方案。
多态
可以简单的理解为 : 父类引用 指向 子类对象。 (向上转型)。
当然还有向下转型的,就是强转,不过这时候需要保证类型所属是正确的,否则就会抛 ClassCast异常。
比如
Animal a = new Bird();
Bird b = (Bird)a; // 这是可以的,类型是正确的。
Animal a2 = new Person();
Bird b2 = (Bird) a2; // 这样就不行了, 人类 不是 鸟类。 会抛 ClassCast 异常。
- instanceof 关键字: 判断某个对象 是不是属于某种类型(或其父类型)。
重写 和 重载 也是属于多态的一种体现。(多态: 多种表现形态)
重写: 字父类中,方法的多态体现。
重载: 一个类中,方法的多态体现。
Object 类
是所有类的 根基类。相当于 对象中 “万物” 的概念。
多态表现:可以接收 任意数据类型的引用。
-
toString()
默认返回是 类名@4554617c 形式。 @后面的数值是 散列码 的 无符号的十六进制表示形式。
推荐重写toString( ), 返回的是人们可读的 字符串表示形式。
-
equals( )
引用类型用 == 判断的是地址是否相等,基本数据类型用 == 判断的是 数值是否相等。
如果要对 引用类型 用人们常理解的相等的方式来判断相不相等,就要重写 equals() 方法。
equals( ) 方法需要满足5个特性:
-
自反性。
x.equals(x) 始终为true。
-
对称性。
x.equals(y) 和 y.equals(x) 始终相等。
-
传递性。
如果有 x.equals(y), y.equals(z) 都为 true
则有 x.equals(z) 为 true。
-
一致性。
在不改变 x,y的情况下, 多次调用 x.equals(y) 始终一致。
-
非空性。
任何非空 x, 都有 x.equals(null) 始终为 false。
重写 equals() 方法,一般都有一套固定的流程。
判断是不是同一个对象引用,判断是不是null,再 类型转换 进行各个属性的判断。
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && name.equals(person.name); }
-
-
hashCode( ) 方法
一般 equals()方法 和 hashCode()方法都是配套使用的。
如果 2个对象 等价(equals 为true),那么他们的 散列码(hash码),一定相等。
但是如果2个对象的 hash码 相等,他们不一定等价。
-
重写 hashCode , 可以用 Objects.hash( ) 方法,这里面用的是 Arrays.hashCode( ) 方法。
这里面的实现原因,就不太清楚了,贴图吧:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19FrTRbE-1588245224177)(C:\Users\Administrator\Desktop\哈希均匀.png)]
-
-
clone()方法
需要重写 clone() 的类,必须实现 Clonable 接口, 否则会抛 CloneNotSurport 异常。
分为 深拷贝、浅拷贝。
如果对象里的属性有 引用类型的数据,他们就有区别了。 (虽然有点绕,但就是这么个区别)
浅拷贝: 拷贝对象, 和原始对象, 他们里面的引用属性 是 指向的同一个对象。
深拷贝: 拷贝对象, 和原始对象, 他们里面的引用属性 是 指向 不同的对象。
一般不会用 clone( ) 方法,既复杂又会抛异常。
Effective Java 书上推荐用 拷贝构造函数、或者 拷贝工厂。
内部类
定义在 类里面 或者 方法里面 的类,就是内部类。
广义的分:
定义在 类 里的:
-
成员内部类
-
静态内部类 (就是在 成员内部类加个 static 关键字修饰)
-
匿名内部类 (局部内部类的限制,同样也限制了他)
定义在 方法 里的:
-
局部内部类
关于内部类的栗子:
(提出来说一点: JDK1.8以后,对于变量,默认都是写了final的,不去改变他那他就是final修饰的了)
public class Main {
public static void main(String[] args) {
// 成员内部类的访问
Outer outer = new Outer();
outer.setX(100);
Outer.Inner inner = outer.new Inner();
inner.say();
System.out.println("================================");
// 静态内部类的使用
Outer.InnerStatic innerStatic = new Outer.InnerStatic();
innerStatic.say();
System.out.println("================================");
// 局部内部类的访问
outer.method();
System.out.println("================================");
/** * 匿名内部类 * 也算是 属于 局部内部类。 * 使用匿名内部类,必须通过 实现一个接口,或者继承一个类的形式来实现。 * * 注意: * 匿名内部类里, 不能有抽象的方法。 * 匿名内部类里, 不能有 static 的资源。 * 匿名内部类里, 只能访问外面的 final 型的局部变量。 * (因为在编译时,类都是单独编译成一个 .class 文件,如果里面有 访问了 非fianl 型的变量,不知道把他存在哪。。。) */
// 匿名内部类的使用
MyInterface myInterface = new MyInterface() {
@Override
public void methodByInterface() {
System.out.println("匿名内部类。。。实现了一个接口");
}
};
myInterface.methodByInterface();
// JDK1.8 之后,对于 变量,是默认就有 final 关键字,如果不改变他,就认为他是一个 fianl 修饰的。
// int num = 666;
// num = 999;
new MyClass(123) {
// public static int aaa;
public void methodByMyClass() {
System.out.println("匿名内部类。。。继承了一个父类");
// 不加这句话: num = 999; 就不报错。。。。
// System.out.println("匿名内部类。。。继承了一个父类" + num);
}
}.methodByMyClass();
}
}
class Outer{
private int x;
private static final String name = "Outer Class String";
/** * 成员内部类。 * 需要依赖 外部类。 * 可访问外部内的一切资源。 如果重名了,就要 外部类.this.资源 的形式访问。 */
public class Inner{
private int x;
private int y;
public void say() {
System.out.println("局部内部类的 : " + x + " ; " + y);
System.out.println("外部类的: "+Outer.this.x);
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
/** * 静态内部类 * 不依赖外部类, 只能用 外部类的 static 资源。 */
static class InnerStatic{
private int x;
public void say(){
System.out.println(x + " ; " + Outer.name);
}
}
public void method() {
/** * 局部内部类 * 就像方法里面的局部变量一样,是不能有 public、private 这些权限修饰符的。 */
class LocalInner{
int x;
public void say() {
System.out.println("局部内部类的方法");
}
}
new LocalInner().say();
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
包装类型
偷个懒,不久前就写过,现在直接放链接吧: https://blog.csdn.net/weixin_43201538/article/details/105419905
可变参数
JDk1.5 之后参数列表 支持可变参数。 就在类型后面加三个小数点就可以。。。
需要注意的是: 可变参数必须放在参数列表的最后一个位置。
(很容易想明白,放前面的话不知道什么时候结束,什么时候调用后面的 固定长度的参数)
递归
就是 自己调用自己调用。 所以要有终止条件,否则很容易 StackOverflowError。
在 二叉树的算法题中,递归的方式就很好用。。。