阿浪与佩奇在Java遇到的muddy puddles

177 阅读8分钟

由于阿浪技术太菜,Java基础不牢,总是写出Bug被人嘲笑。

于是下定决心奋发图强,在犄角旮旯里翻出大学时压箱底的神书《Thinking in Java》一番恶补!!!

1、父子关系下的对象指代,及对象加载的动态绑定

在学习Java继承知识的时候都会接触到This和Super两个关键字。一般的,This指代本类对象引用,Super指代父类对象引用。

让我们用代码说话,看看下列代码输出结果是啥?

public class Father {
    Father() {
        this.playA();
    }
    public void playA() {
        System.out.println("Daddy pig");
    }
    public void playB() {
        System.out.println("Daddy exclusive");
    }
}

public class Child extends Father{
    Child() {
        super.playA();
    }
    @Override
    public void playA() {
        System.out.println("Peppa pig");
    }
    public void playC() {
        System.out.println("Peppa exclusive");
    }
    public static void main(String[] args) {
        Father target = new Child();
        target.playA();
        target.playB();
        ((Child) target).playC();
    }
}

子类构造器在调用父类构造器时,父类有且只存在有参构造器,子类才需调用Super(xxx)方法。

因为子类通过构造器创建对象并初始化时,总是去向上寻找父类构造器隐式调用。若未找寻到匹配构造器(有参/无参)将编译出错。

打印结果:
Peppa pig
Daddy pig
Peppa pig
Daddy exclusive
Peppa exclusive

通过结果我们看到,当父类构造器在调用父类palyA()方法时,实际执行的是重写的子类playA() ,只有通过 super.方法() 形式直接调用指定的父类方法才会执行原方法。

而且子类实例在向上转型到父类引用后,调用子类独有方法时,IDE会自动提醒我们强制类型转换到方法所在实体类。

  • 这时候阿浪就犯迷糊了,为什么target引用变量一会儿能直接调用到子类重写方法,一会儿又要强转成子类类型调用呢?

动态绑定: 也叫后期绑定。在运行时,JVM根据具体对象的类型进行绑定,当对象在堆中被创建了之后,才能确定方法属于哪一个对象。Java里除了static方法和final方法都是动态绑定。

2、 自动装箱/拆箱,及常量池

日常开发过程中,数据类型是离不开的。阿里的开发手册推荐所有POJO属性和RPC参数全使用引用类型,所有局部变量使用基本类型。因此开发过程中就经常遇到两种类型赋值和比较的场景。

基本数据类型 引用数据类型
boolean Boolean
char Character
byte Byte
short Short
int Integer
float Float
long Long
double Double

从代码中只能看到字面量直接赋值给了引用类型,且与基本类型能够直接数据交换,而且不需要做什么额外的类型转换。

static void Conversion() {
        Integer A = 1;
        int a = A;

        Double B = 2.2;
        double b = B;

        Boolean C = true;
        boolean c = C;
        
        if (2 >= A){}
    }

虽然知道这是因为JDK的自动拆箱/装箱特性提供的便利,但具体是如何实现的呢?用jd-jui来反编译class文件来看一下:

static void Conversion()
  {
    Integer A = Integer.valueOf(1);
    int a = A.intValue();
    
    Double B = Double.valueOf(2.2D);
    double b = B.doubleValue();
    
    Boolean C = Boolean.valueOf(true);
    boolean c = C.booleanValue();

    if (2 >= A.intValue()) {}
  }

由反编译出来的代码可以看出,编译器把类型转换的所有脏活累活都做了,我们只需要安静的品尝这一特性带来的语法糖。


除了八种引用数据类型,还有一种最常用的引用类型String。当我们在使用这些数据类型时,它们是以何种形态存储在内存中的呢?

数据类型 位数 可表示数据范围 默认值
byte 8 -128~127 0
short 16 -32768~32767 0
int 32 -21..8~21..7 0
long 64 -9...8~9...7 0
float 32 -3.4E38~3.4E38 0.0
double 64 1.7E308~1.7E308 0.0
char 16 0~255 \u0000
boolean - true/false false
string - 65535 null

2.1、 字符串常量池

JVM堆内存除了存储对象实例还存储字符串常量池。

  1. 通过字面量直接赋值String创建字符串时,首先会去字符串常量池内寻找是否已存在该字面量值。若存在,String直接指向该值所在内存。否则,常量池创建该字面量值,且返回常量池内指向地址,且不会在堆内创建任何对象。

  2. 通过new一个String实例创建字符串时,同样去常量池内寻找。如果存在,则直接在堆内创建一个对象,且返回堆内对象地址给引用变量。若不存在,首先在常量池内创建该字面量值,然后在堆内创建对象,返回堆内地址给引用变量。

static void Compare() {
        String A = "zj";
        String B = "zj";
        System.out.println(A == B); //TRUE

        String C = new String("alang");
        String D = new String("alang");
        System.out.println(C == D); //FALSE
    }

2.2、局部变量表

当方法被调用时,JVM会在虚拟机栈创建一个栈帧,方法的调用和结束对应着栈帧的入栈和出栈。栈帧内部又存储着 局部变量表操作数栈动态链接方法出口

局部变量表是方法调用时管理变量的司令部,内部存储 基本数据类型、对象引用(并不代表对象本身,可能是指向对象起始地址的引用指针,也可能是只想一个代理对象的句柄)、returnAdress。

局部变量表所需内存空间在编译期间完成分配。

2.3、运行时常量池

运行时常量池存在于方法区内,以前是持久代,JDK1.8后方法区由元空间实现,既本地内存。 运行时常量池在类加载后存放编译时期生成的各种 字面量符号引用

3、 莆田系的Arrays.asList()

3.1、不能直接使用 Arrays.asList() 来转换基本类型数组

直接从asList的源码可以看到:

public static <T> List<TasList(T... a) {
    return new ArrayList<>(a);
}

Arrays.asList() 方法传入的是一个泛型 T 类型的可变参数,众所周知基本数据类型无法做泛型。因此当我们直接转换一个int[]数组时,并不能得到一个想要的List,只能得到元素个数为1,且整个元素是整数数组的List。

解决方法:
1、使用Arrays.stream
2、把int转为Integer

3.2、Arrays.asList 返回的 ArrayList 不支持增删操作

啥也不说上源码:

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }
}

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

public E remove(int index) {
        throw new UnsupportedOperationException();
    }
}

从源码可知,这与我们平时用的java.util.ArrayList不是一个List,今日我李逵终遇这李鬼。这个 ArrayList是Arrays 的内部类。

ArrayList 内部类继承自 AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出 UnsupportedOperationException。

因此,当我们在对Arrays.asList()得到的List进行 add()remove() 操作时,不光无效还会抛出UnsupportedOperationException异常。

3.3、对原始数组的修改会影响到我们获得的 ArrayList

从上面的源码可以看出,Arrays.asList()方法返回的Arraylist,在通过构造函器创建List时是直接对原始数组直接操作,a = Objects.requireNonNull(array)。

解决方法: List list = new ArrayList(Arrays.asList(数组));