Java类与继承

  笔记篇

继承的概念

继承所表达的就是一种对象类之间的相交关系,它使得某类对象可以继承另外一类对象的数据成员和成员方法。若类B继承类A,则属于B的对象便具有类A的全部或部分性质(数据属性)和功能(操作),我们称被继承的类A为基类、父类或超类,而称继承类B为A的派生类或子类。

-by 百度百科

完全继承

  子类继承父类所有的属性和方法(包括private)。Java的访问修饰符只是用于限制类中的属性或者方法的访问权限的,与是否被继承并无关(即子类无法访问父类的私有的属性和方法)。从内存上讲,创建子类对象时,底层隐式创建父类对象,父类对象自然拥有所有的属性和方法。对于父类final方法,定义是被final修饰的方法是无法被覆盖(重写)

通过反射获取私有方法(属性同理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package test;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;

public class MainTest {
public static void main(String[] args) throws Exception {
//获取子类
Class clazz = Class.forName("test.Children");
//得到父类
Class superClass = clazz.getSuperclass();
//得到父类非继承的所以方法
Method[] methods = superClass.getDeclaredMethods();
//设置私有方法可以被访问
AccessibleObject.setAccessible(methods,true);
for (Method m:methods) {
System.out.println("\n子类调用方法"+m.getName()+"()的调用结果:");
m.invoke(new Children());
}

}
}
class Parent{
Parent() {
System.out.println("调用父类构造方法!");
}
private static void staticParent() {
System.out.println("调用父类静态方法");
}
protected final void finalParent() {
System.out.println("调用父类final方法");
}
private void printParent(){
System.out.println("调用父类私有方法");
}
}
class Children extends Parent{
public void printChild(){
System.out.println("调用子类公有方法");
}
}

Console:

子类调用方法finalParent()的调用结果:
调用父类构造方法!
调用父类final方法

子类调用方法printParent()的调用结果:
调用父类构造方法!
调用父类私有方法

子类调用方法staticParent()的调用结果:
调用父类构造方法!
调用父类静态方法

子类创建过程中,内存的加载顺序

  收集和记录一些相关问题和解析。

父类构造方法调用子类覆盖方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainTest {
public static void main(String[] args) throws Exception {
Children children = new Children();
}
}
class Parent{
Parent() {
printChild();
}
public void printChild(){
System.out.println("调用父类方法");
}
}
class Children extends Parent{
int testNum = 100;
@Override
public void printChild(){
System.out.println("调用子类方法 "+ testNum);
}
}

Console:

调用子类方法 0

优点:通过继承相同的父类,初始化子类时,父类会调用不同子类的不同复写方法,从而实现多态性。
缺点:如果在父类构造函数中调用被子类重写的方法,会导致子类重写的方法在子类构造器的所有代码之前执行,从而导致子类重写的方法访问不到子类实例变量的值,因为此时这些变量还没有被初始化。
下图是,早期和群友吹水画的,凑活着看。
构造方法调用多态方法
可以看到,当进行到第8步时,父类构造调用子类方法是正常进行的,父子类的方法表早已载入方法区,但子类的实例变量尚未初始化。(类的实例变量初始化是在构造器的顶部执行的,但super()是子类构造器的第一句。)

关于父类引用指向子类实例对象的问题

  • 网上前辈的几点总结:
  1. 使用父类类型的引用指向子类的对象;
  2. 该引用只能调用父类中定义的方法和变量;
  3. 如果子类中重写了父类中的一个方法,那么在调用这个方法的时候,将会调用子类中的这个方法;(动态连接、动态调用)
  4. 变量不能被重写(覆盖),“重写”的概念只针对方法,如果在子类中“重写”了父类中的变量,那么在编译时会报错。
  • 多态的三个必要条件:
    1.继承 2.重写 3.父类引用指向子类对象。

父类引用指向子类实例对象的内存分析

假设现在有一个父类Father,其自身的变量需要占用1M内存.
有一个它的子类Son,其自身的变量需要占用0.5M内存. 即
Father f = new Father(); //占用1M内存
Sun s = new Sun(); //占用1.5M内存
因为子类中有一个隐藏的引用super会指向父类实例,所以在实例化子类之前会先实例化一个父类,也就是说会先执行父类的构造函数.由于s中包含了父类的实例,所以s可以调用父类的方法。
Father f1 = (Father)s;
这时f1会指向那1.5M内存中的1M内存,即是说,f1只是指向了s中实例的父类实例对象,所以f1只能调用父类的方法(存储在1M内存中),而不能调用子类的方法(存储在0.5M内存中)。
Son s1 = (Son)f;//这句代码运行时会报ClassCastException.
因为f中只有1M内存,而子类的引用都必须要有1.5M的内存,所以无法转换.
Son s3 = (Son)f1;//这句可以通过运行,这时s3指向那1.5M的内存.
由于f1是由s转换过来的,所以它是有1.5M的内存的,只是它指向的只有1M内存.
本段来源 百度知道

动态方法查找

  当父类中的一个方法只有在父类中定义而在子类中没有被重写的情况下,才可以被父类类型的引用调用;对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态方法查找。

‘动态方法查找’ 我使用这个名词源自于《写给大忙人看的Java核心技术》书的4.1.5中,在网上的其他博客中,在谈及时,名字都有所不同,又或叫’动态链接’,’动态绑定’

编译看左,运行看右

  • 在多态中,成员变量的特点: 无论编译和运行,都参考左边(引用型变量所属的类)
  • 在多态中,静态成员函数的特点:无论编译和运行,都参考做左边。
  • 在多态中,非静态成员函数的特点:编译看左边,运行看右边。

所有类的祖宗: Object

Java中的所有类都直接或间接地继承自Object类。当没有显示的声明一个类的父类时,他会隐式地继承Object。

toString方法

  • Object类定义toString()方法打印类名@哈希码;
  • 比起obj.toStrnig(),直接写””+obj会更好,避免了NPE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //对于+操作符,java.lang.StringBuilder.append(Object obj)
    public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
    }

    //对于直接打印对象,java.io.PrintSteam.println(Object x)
    public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
    print(s);
    newLine();
    }
    }

    //java.lang.String.valueOf(Object obj)
    public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
    }

equals方法

只有在基于状态的相等性测试中,才有必要覆盖equals方法。在这种测试中,当两个对象有同样的内容时,则认为它们是相等的。
覆盖equals方法,必须提供一个兼容的hashcode方法

在覆盖的时候,必须要遵守它的通用约定:

  • 自反性。对于任何非null的引用值x,x.equals(x)必须返回true
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)则必须返回true。
  • 一致性。对于任何非null的引用值x、y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一直返回true,或者一致返回false。
  • 对于任何非null的引用值x,x.equals(null)必须返回false。

hashcode方法

重点在于hashmap(小声逼逼)
如何正确的重写equals() 和 hashCode()方法

hashCode 的常规协定是:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

clone方法

Object.clone只做了浅拷贝。即,从元对象中拷贝所有实例变量到拷贝对象。对于基本变量,则直接拷贝其数值。对于引用类型,原对象和拷贝对象共享同个引用。(简单画个图……不要在意String类型)

浅拷贝:

浅复制

深拷贝:

深复制
想彻底深拷贝,必须要实现Cloneable接口,覆盖clone方法,并且在clone方法内部,把该对象引用的其他对象(实现Cloneable接口)也要clone一份。
在此就不做演示了,因为很少在开发的时候需要这样做。

zcolder wechat
写得不好?加我QQ开始喷我!