开篇
Java是一门不那么简单也不那么复杂的语言,Java里面有很多问题和特性是容易被使用者忽视的,这些问题也许会难住新手,同时也许会是老手不小心跌入的无故之坑,只有精于对基础的提炼才能最大程度地解决类似的疑问。所以,在读Cay.Horstmann的《Java核心技术》的过程中,我记录下这些所谓的易忽略的问题,这些问题将会持续更新在我的这个Segment Fault的博客下,也算是激励自己重新挖掘这些基础问题的内涵。这个博客将以原书中的章节为分割,大概会是每章一篇,持续更新,每篇的内容也不会一次全部写完,视我个人对问题的理解和我的阅读进度而定。

Core Java Volume 1 Key Points

chap5

super关键字和this

super这个关键字和this其实是完全不同的,因为this实际上是一个真实存在的引用,是一个缺省的传入方法的隐式参数,引用当前的对象,所以可以完全当做一个正常的引用来使用。而super并不是父类对象的引用,而只是给javac编译器的一个提示性质的标志。当使用如下的super时:

  • super.someFunction(); 提示javac在编译这个someFunction()方法的时候去使用该类的父类定义的someFunction()方法,这一般是在当前的子类也定义了同样名字的someFunction()方法的时候去使用的。

  • super();或者自定义的super(type Par); 提示javac在编译的时候使用当前子类的父类定义的构造器去初始化当前对象。

所以,总结起来,super的用法归为两种:一是可以调用父类构造器,二是可以调用父类方法。

子类对象的初始化过程:子类执行子类的初始化方法,初始化方法里会先执行缺省的super()方法(写不写这个super方法都会执行),也就是说会先初始化出一个父类对象,然后再执行其他的初始化部分,最终之前初始化出的父类对象将会成为子类对象,这也是多态性的起始,因为本质上来讲,子类对象是有父类对象的结构的。

继承存在情形下的对象初始化

在继承存在的时候,初始化的过程就变得异常复杂,但是却仍然遵循着初始化的原则:先加载类和属于类的static属性,然后创建类的对象,因为有继承的问题,所以在初始化子类对象的时候要先初始化其父类的对象。
这个例子极佳地说明了这个复杂的过程,可以使用单步调试给出初始化的全过程:

class FatherClass {
    
    private static FatherClass f = new FatherClass();
    static {
        b = 10;
        System.out.println("father static block");
    }
    
    {
        System.out.println("father object block");
    }
    
    static int b = 5;
    
    public FatherClass() {
        System.out.println("father constructor...");
        System.out.println( "b = " + b);
        
    }
}

public class ChildClass extends FatherClass {
    static int a = 5;
    static {
        a = 10;
        System.out.println("child static block");
    }
    
    {
        System.out.println("chid object block");
    }
    
    private static ChildClass t1 = new ChildClass();
    
    public ChildClass() {
        System.out.println("child constructor...");
        System.out.println("a = " + a);
    }
    
    public static void main(String[] args) {
        ChildClass test = new ChildClass();
    }
}

这是运行的初始化结果:

先加载类,所以顺序执行static代码块和static属性声明,按照先父类再子类的顺序加载类,

father object block
//加载父类执行f的初始化时激发了父类对象的初始化,对象的初始化块执行
father constructor...   
//对象的构造器
b = 0
//未经初始化的属性的缺省值
father static block
//f的初始化完成,执行父类的静态初始化快,父类加载完毕
child static block
//子类初始化开始,执行子类的静态初始化块
father object block
//t1的初始化激发了子类对象的初始化,并进而激发了父类对象的初始化,执行父类对象初始化块
father constructor...
//执行父类的构造器
b = 5
//父类的域属性已经被初始化,父类对象初始化完成
chid object block
//子类对象开始初始化,对象初始化块执行
child constructor...
//子类对象的构造器执行
a = 10
//子类对象的域属性已被初始化,完成子类的加载
father object block
//执行父类对象的初始化
father constructor...
b = 5
chid object block
//执行子类对象的初始化
child constructor...
a = 10

多态是怎么实现的

如果使用最简单的办法去说明什么是多态,那么这样写无疑是有力的:

FatherClass child = new ChildClass();
child.funcOverride();

这个的意思就是说栈中创建的父类对象可以引用堆中的子类对象,那么这个过程为什么可以实现呢?从两个方面可以说明:
从对象初始化的角度来说,和上一节我们说到的一样,子类对象的初始化意味父类先加载,子类再加载,父类对象初始化,子类对象初始化,所以子类对象实际上就是在父类对象的基础上生成的,因此父类对象当然可以引用子类对象了;
从方法覆盖的角度来说,父类对象的方法子类对象都有,因此对父类方法的使用实际上就会成为对子类方法的使用,这个过程不能反过来,也就是说子类对象的方法并不是父类对象都有,因此不能使用子类对象去引用父类对象。

jvm为了实现多态情况下方法执行的快速判断,会为每个类维护一个虚方法表(和C++实现多态的虚拟函数有关),这个方法表表述了该类型对象在执行某方法时应该执行的方法具体是哪个,理论上在child去执行funcOverride()方法的时候会先去看ChildClass是否拥有覆盖的funcOverride()方法,如果有则执行这个,如果没有覆盖则执行父类的同名方法,而实际上为避免做这些可能要进行多次的判断,jvm为类准备的虚方法表固化这种方法对应关系,进而可以快速定位要执行的方法。

举个例子:

class FatherClass{
    public void func1(){
        System.out.println("func from FC");
    }
    public void func2(){
        System.out.println("func from FC");
    }
}

class ChildClass extends FatherClass{
    @Override
    public void func1() {
        System.out.println("func from CC");
    }
}

那么,jvm为类维护的虚方法表类似(不要去考虑和这个问题无关的Object自带方法):

VirtualMethodTableOfFatherClass:
FatherClass.func1()     --      FatherClass.func1()
FatherClass.func2()     --      FatherClass.func2()

VirtualMethodTableOfChildClass:
ChildClass.func1()     --      ChildClass.func1()
ChildClass.func2()     --      FatherClass.func2()

一个完善的equals方法怎么写呢?

每个类都源自Object类,所以也就不得不接受Object父类带来的基本方法,比如这个麻烦的额equals方法,在Object类中的equal方法非常简单,就是一句话:

public boolean equals(Object obj) {
    return (this == obj);
}

这句话用于判断当前这个栈中变量引用的堆中对象是否和参数变量引用的堆中对象和同一个对象,所以这其实是个很抽象的“相等”,因为这就像废话一样。

所以一个完善的equals方法应该恰当地指出两个对象相等的内涵,即时两个变量并不是指向同一块内存,如果他们的某些被指定的属性是一样的,那么就可以算作是“相等”。所以,我们有以下这样对对象相等的考虑:
1.是否指向同一块堆中内存空间,也就是是否引用同一对象,如果是,这肯定相等;
2.是否是空引用的比较,如果不是,则才可能相等;
3.是否具有同样的类型,如果是,则才可能相等;
4.是否具有继承链上的一系列类型,如果是,则可能相等;
5.是否某些属性是相等的,如果是,则才可能相等。
基于上面的考虑,我们可以有这样的例子来写出一个相对完善的equals方法:
(特别注意的是,equals的参数是Object类型的,只有这样才能覆盖Object的equals方法)

class Employee{
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public boolean equals(Object obj) {
        if(this  == obj)
            return true;
        if(obj == null)
            return false;
        if(this.getClass() != obj.getClass())
            return false;
        Employee tempEmployee = (Employee)obj;
        return tempEmployee.getName().equals(name);
    }
}
public class TestClass {
    public static void main(String[] args) {
        Employee employee1 = new Employee();
        employee1.setName("boss");
        Employee employee2 = new Employee();
        employee2.setName("boss");
        System.out.println(employee1.equals(employee2));
        System.out.println(employee2.equals(employee1));
        
    }
}

ArrayList之类的集合是值拷贝的和还引是用拷贝的

我们都知道ArrayList这样的数据集合就是Java快速处理批量数据的容器,那么对这个容器里的每个数据元素而言,是把每个元素的值拷贝到容器中呢还是只是把它们的引用拷贝到容器中去呢?从效率和资源占用的角度来说,ArrayList选择了引用拷贝,只是把“构成”这个集合的数据元素的引用放到了容器中,这个例子说明了这个问题:

并不是每次“添加”一个数据元素到容器中,而是“引用”一个元素到容器中,如果每次都拿一个数据元素做引用,最后所有的容器元素索引都指向一个引用的地址。

class Employee {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
public class TestClass {
    public static void main(String[] args) {
        Employee employee = new Employee();
        ArrayList<Employee> list = new ArrayList<Employee>();
        for (int i = 0; i < 5; i++) {
            employee.setName("name" + i);
            list.add(employee);
        }
        for (Employee each : list) {
            System.out.println(each.getName());
        }
    }
}

未完成。。。


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员