Java的十万个为什么系统化重学 Java 之基础篇

Posted 盛夏温暖流年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java的十万个为什么系统化重学 Java 之基础篇相关的知识,希望对你有一定的参考价值。

打破舒适圈,经常对 Java 知识进行总结思考,才能保证自己不掉队。

日常工作中,我们很容易会陷入 “大家都这么用,所以我也这么用” 的舒适圈中,久而久之就会安于现状而失去了思考的能力。

凡事多问为什么,寻找背后的答案,是一个优秀程序员最基本的素养,之前的我没有做好,但是未来的我一定要做到。

这是第一篇,我会坚持更新下去,每个模块的博客也会持续补充和完善,系统化重学 Java,一起出发吧!


第一问: 为什么子类使用 super 调用父类构造方法要放在第一行?

在分析之前,首先要了解代码的加载顺序:

由上图可以得出,系统在调用子类的构造方法前,会先去调用父类的构造方法;

编辑器在构造子类时,首先会去检测第一行是否显示调用了父类的构造方法,如果没有检测到,就默认调用父类的无参构造方法来进行初始化。

那假如我们在第二行调用了父类的构造方法,就会出现重复调用的问题,所以建议放在第一行以保证子类能够被正确地初始化。


第二问: 为什么说 Java 中只有值传递?

在 Java 中,方法传递的实质上是 参数值的拷贝

(1). 对于基本数据类型来说,在方法中对参数进行操作(比如交换),不会影响原始值;

测试代码如下:

public class ValueDemo {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        swap(a, b);
        System.out.println("原始参数:a = " + a);
        System.out.println("原始参数:b = " + b);
    }

    private static void swap(int a, int b) {
        int tmp = a;
        a = b;
        b = tmp;
        System.out.println("方法中的拷贝参数:a = " + a);
        System.out.println("方法中的拷贝参数:b = " + b);
    }
}

运行结果:

可以看出,基本数据类型 a 和 b 交换后,并没有改变原始值;

(2). 对于引用类型来说,可以改变引用参数的状态,但是不能改变存储在变量中的对象引用本身。

测试代码如下:

public class ValueDemo {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = {4, 5, 6};
        changeStatus(a);
        changeObject(a, b);
        System.out.println("原始对象引用参数:a = " + Arrays.toString(a));
        System.out.println("原始对象引用参数:b = " + Arrays.toString(b));
    }

    private static void changeStatus(int[] a) {
        a[0] = 2;
        System.out.println("方法中改变a的状态 = " + Arrays.toString(a));
    }

    private static void changeObject(int[] a, int[] b) {
        int[] temp = a;
        a = b;
        b = temp;
        System.out.println("方法中的对象引用参数:a = " + Arrays.toString(a));
        System.out.println("方法中的对象引用参数:b = " + Arrays.toString(b));
    }
}

运行结果:

可以看出,我们可以修改 a[0] 的值,也就是改变了引用参数的状态;

但是引用类型参数 a 和 b 交换后,没有改变存储在变量中的对象引用本身。


第三问: 为什么重写 equals 一定要重写 hashCode?

首先我们要知道, hashCode 方法的常规约定是:如果两个对象相等,那么它们的哈希码一定相等。不过这只是个约定,个人认为更底层的原因是因为如果不重写,会导致特定应用场景下的逻辑错误。

比如在 HashMap 中判断元素是否相等时,为了缩小查找成本,首先会去判断 hashCode 是否相等,之后才会调用 equals 方法判断元素是否真正相等。

所以为了避免我们认为相等,但是逻辑判断却不相等的情况出现,重写 equals 一定要重写hashCode 方法。


第四问: 为什么经常会说 Java 是编译与解释共存的语言?

首先,我们知道,高级语言按程序的执行方式可分为 编译型语言 和 解释型语言,以下是具体的定义:

编译型语言:在程序执行前,先使用编译器对程序进行编译,将其转换成可直接执行的机器语言。这样运行时就不需要翻译,可以直接执行。

解释型语言:在程序执行时,通过解释器对程序逐行作出解释,然后直接运行。

而 Java 程序从源代码到运行一般有下面 3 步:

可以看出,Java 首先会将源代码通过 javac 命令编译成字节码(class文件),之后在运行时, JVM 会将字节码解释为对应机器码,最后执行。

因为有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面 Java 引进了 JIT (运行时编译器),它能够捕获程序中的热点代码,将其编译成机器码并缓存起来。

当遇到相同的代码时,就不必再去使用解释器进行解释,而是直接查找对应的机器码执行,这样就可以避免解释器重复多次的解释执行。

从这个角度来看,我们会说 Java 是编译与解释共存的语言。


第五问: 为什么 Java 中的 “饿汉式” 单例模式更推荐 “枚举” 的实现方式?

我们知道, Java 中的单例模式是一种设计模式,它能够保证整个系统中的某个类只有一个实例对象可以被获取和访问。

而单例模式又可以分为 “饿汉式” 和 “饱汉式” 两种:

饿汉式:立即直接创建对象,不会存在线程安全问题;

饱汉式:延迟创建对象,第一次使用时再进行创建;

饿汉式的三种创建类型分为:直接实例化,枚举,静态代码块;

直接实例化:

public Class SingletonDemo1{
    private static SingletonDemo1 INSTANCE = new SingletonDemo1();
    private SingletonDemo1(){
    }
    
    public static SingletonDemo1 getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args){
    	SingletonDemo1 instance = getInstance();
    }
}

枚举:

public enum SingletonDemo2{
    INSTANCE;
}

class Test{
    public static void main(String[] args){
    	SingletonDemo2 instance = SingletonDemo2.INSTANCE;
    }    
}

静态代码块:

public class SingletonDemo3{
    
    private static SingletonDemo3 INSTANCE;

    {
        INSTANCE = new SingletonDemo3();
        // 其他操作
        ...
    }

    private SingletonDemo3(){}

    public static SingletonDemo3 getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args){
        SingletonDemo3 instance = SingletonDemo3.getInstance();
        SingletonDemo3 instance2 = SingletonDemo3.getInstance();
        System.out.println(instance == instance2);
    }

}

 可以看出,枚举方式的代码量是最少的,只要 Java 版本大于 JDK 1.5,建议使用这种方式进行实现。


第六问: 为什么多线程场景下的 “饱汉式” 单例模式更推荐 “静态内部类” 的实现方式?

首先要知道,饱汉式的三种创建类型分为:线程不安全的创建,线程安全的创建,静态内部类;

线程不安全的创建:

public Class SingletonDemo4{
    private static SingletonDemo4 INSTANCE;
    
    private SingletonDemo4(){
    }
    
    public static SingletonDemo4 getInstance(){
        if(INSTANCE == null){
            INSTANCE = new SingletonDemo4();
        }
        return INSTANCE;    
    }
}

既然要求的是多线程场景下,就不能选择这种线程不安全的单例模式实现;

线程安全的创建:

public class SingletonDemo4{
    private static SingletonDemo4 INSTANCE;
    
    private SingletonDemo4(){
    }
    
    public static synchronized SingletonDemo4 getInstance(){
        if(INSTANCE == null){
            INSTANCE = new SingletonDemo4();
        }
        return INSTANCE;    
    }
}

可以看出,我们使用 synchronized 修饰 getInstance() 方法保证了线程安全,但是加锁才能保证单例,影响效率;

静态内部类:

public class SingletonDemo5 {

    private SingletonDemo5() {
    }

    private static class InnerClass {
        private static SingletonDemo5 INSTANCE = new SingletonDemo5();
    }

    public static SingletonDemo5 getInstance() {
        return InnerClass.INSTANCE;
    }

    public static void main(String[] args) {
        SingletonDemo5 instance1 = SingletonDemo5.getInstance();
        SingletonDemo5 instance2 = SingletonDemo5.getInstance();
        System.out.println(instance1 == instance2);
    }
}

静态内部类只有在加载的时候才会创建实例对象,也没有性能问题,建议使用。


第七问: 为什么同样是自动装箱,Integer 的值有的相等,有的不等?

假如有以下代码:

public class Test {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);

    }
}

运行后,打印结果如下:

明明只差了 1,同样是自动装箱,为什么结果却不一样?

大家都知道 String 有一个常量池,用来存放所有的 String 对象,实质上 Integer 也有这样一个常量池存在。

而我们用 Integer a = 127 的方式创建一个 Integer 类时,Java 会调用方法 Integer.valueOf(),它的内部缓存了 - 128 到 127 范围内的 Integer 类。

当我们给这个范围内的封装类赋值时,装箱操作会赋上一个缓存池实例的引用

但是如果用超出这个范围的值给封装类赋值时,Integer.valueOf() 会创建一个新的 Integer 类。

因此,对比超出这个范围的 Integer 类时,自然会返回 false。


第八问: 深拷贝与浅拷贝中,为什么说要慎用浅拷贝?

首先我们要知道对象拷贝是什么:

​对象拷贝 (Object Copy) :将一个对象的属性,拷贝到另一个有着相同类类型的对象中去,目的主要是为了在新的上下文环境中复用对象的部分或全部数据。

再来看看浅拷贝和深拷贝的定义:

浅拷贝

对基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般的拷⻉。

也就是对于引⽤数据类型来说,原始对象和新对象引用同一对象,新对象中的引用型字段发生变化,会导致原始对象中的对应字段也发生变化。

深拷贝

对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制内容。

只看定义不够清晰,我们用代码来说明一下。

浅拷贝代码实现

友情提示:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

定义一个人员信息类 Person,包含了地址类 Address

public class Person implements Cloneable{

    private String name;
    private Address address = new Address();

    public Person(){}

    public Person(String name, String province, String city){
        this.name = name;
        this.address.setProvince(province);
        this.address.setCity(city);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person[name=" + name + ", address=" + getAddress() + "]";
    }
}

class Address{
    private String province;
    private String city;

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}

 主函数调用:

public class TestCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 原始对象
        Person person1 = new Person("小张同学", "山东省", "烟台市");
        // 拷贝对象
        Person person2 = (Person) person1.clone();

        System.out.println("原始对象:" + person1);
        System.out.println("拷贝对象:" + person2);

        // 对拷贝对象进行修改
        person2.getAddress().setCity("青岛市");
        System.out.println("===================================修改地址属性后====================================");
        System.out.println("原始对象:" + person1);
        System.out.println("拷贝对象:" + person2);
    }
}

调用结果:

 可以得出结论:

浅拷贝改变拷贝对象的引用数据类型 Address 的 city 属性,原始对象中的对应字段也会发生变化; 

深拷贝代码实现

只需让 Address 类实现 Cloneable 接口,并且覆写 clone 方法(参考 Person 类的实现即可);

改写 Person 中的 clone 方法如下:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone();
        // 实现深拷贝
        Person person = (Person) super.clone();
        person.setAddress((Address) address.clone());
        return person;
    }

再次运行:

 可以得出结论:

深拷贝改变拷贝对象的引用数据类型 Address 的 city 属性,原始对象中的对应字段不会发生变化;

而 Java 中对象的 clone 方法默认是浅拷贝,若想实现深拷贝就需要重写 clone 方法实现属性对象的拷贝。

所以我们要在日常写代码的时候一定要慎用浅拷贝,不要因为修改浅拷贝对象中的引用字段,而导致原始对象中的对应字段也发生变化。

本问题来自小伙伴:


以上是自己在复习 Java 知识中的疑问点,大家也可以在评论区留下自己在学习中的问题。

我会定期更新补充到博客中,希望通过这个过程,我们可以一起进步和提高 ~

以上是关于Java的十万个为什么系统化重学 Java 之基础篇的主要内容,如果未能解决你的问题,请参考以下文章

无聊之作 对 手游十万个为啥 的解包分析笔记 游戏引

对 Java 的总结和思考系统化重学 Java 之集合篇

重学java基础第八课:硬件和冯洛伊曼系统

重学java基础第八课:硬件和冯洛伊曼系统

Java集合与数据结构——Map & Set 习题练习

重学java基础第七课:什么是计算机