Object类

Object 是 Java 中的根类,是所有类的爹!类中的方法如下:

图片说明

equals方法

在 Object 类中,== 运算符和 equals 方法是等价的,都是比较两个对象的引用是否相等 public boolean equals(Object obj) { return (this == obj); }。从另一方面来讲,如果两个对象的引用相等,那么这两个对象一定是相等的。对于我们自定义的一个对象,如果不重写 equals 方法,那么在比较对象的时候就是调用Object 类的 equals方法,也就是用==运算符比较两个对象。在 Java 规范中,对equals方法的使用必须遵循以下几个规则:

  • 自反性:对于任何非空引用值X,X.equals(X)都应返回true
  • 对称性:对于任何非空引用值X和Y,当且仅当Y.equals(X)返回true时,X.equals(Y)也应该返回true
  • 传递性:对于任何非空引用值X,Y,Z,如果X.equals(Y)返回true,并且Y.equals(Z)返回true,那么X.equals(Z)应返回true
  • 一致性:对于任何非空引用值X和Y,多次调用X.equals(Y)始终返回true或始终返回false

==与equals()的区别

前者的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存地址);equals():它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况2:类覆盖了 equals()方法。覆盖equals()方法来判断两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。

对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法;对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。

为什么重写 equals 方法就必须重写 hashcode 方法

当不重写 equals 方法时,判断两个对象是否相等,等同于利用 == 判断。当重写 equals 方法时,判断两个对象是否相等时则必须调用 equals 方法返回结果。考虑不重写 hashcode 时,虽然两个属性值完全相同的对象得到了相等的结果,但是当把这两个对象加入到 HashSet 或 HashMap 中则会产生异常。就结果而言,两个内容意义上相等的不同对象加入 HashSet 中,容量应该为1,但是 HashSet 取使用 hashcode 判断对象是否已存在或相等所以造成了歧义,最差的结果会导 致HashSet 的不正常运行。所以重写 equals 方法必须重写 hashcode 方法。

hashcode方法

hashcode() 是 Object 类中的函数,所有类都拥有该函数,用于返回该对象的hash值。哈希码的通用约定如下:

  • 在 java 程序执行过程中,在一个对象没有被改变的前提下,无论这个对象被调用多少次,hashCode 方法都会返回相同的整数值。对象的哈希码没有必要在不同的程序中保持相同的值
  • 如果 2 个对象使用 equals 方法进行比较并且相同的话,那么这 2 个对象的 hashCode 方法的值也必须相等
  • 根据 equals 方法,得到两个对象不相等,那么这 2 个对象的 hashCode 可以相同或不同。但是,不相等的对象的 hashCode 值不同的话可以提高哈希表的性能

在 JDK6 与 JDK7 中基于随机数作为 hashcode 的来源; JDK8 默认通过和当前线程油管的一个随机数+三个确定值,运用 Marsaglia’s xorshift scheme 随机数算法得到的一个随机数。

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
    // 根据Park-Miller伪随机数生成器生成的随机数
     value = os::random() ;
  } else
  if (hashCode == 1) {
     // 此类方案将对象的内存地址,做移位运算后与一个随机数进行异或得到结果
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) {
     value = 1 ;            // 返回固定的1
  } else
  if (hashCode == 3) {
     value = ++GVars.hcSequence ;  // 返回一个自增序列的当前值
  } else
  if (hashCode == 4) {
     value = cast_from_oop<intptr_t>(obj) ;  // 对象地址
  } else {
     // 通过和当前线程有关的一个随机数+三个确定值
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }
  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。对于一些容器,尤其是 HashMap 来说,为了保持 Key 唯一性且降低比较次数,自然而然地引入了 hash 值。以 HashMap 的 put 为例:如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的 equals 方法与新元素进行比较,相同的话就不存了;不相同的话,也就是发生了 Hash key 相同导致冲突的情况,那么就在这个 Hash key 的地方产生一个链表,将所有产生相同 HashCode 的对象放到这个单链表上去,串在一起(很少出现)。

clone方法

clone 方法是创建并且返回一个对象的复制之后的结果。复制的含义取决于对象的类定义。clone 方法首先会判对象是否实现了 Cloneable 接口,若无则抛出 CloneNotSupportedException 异常,最后会调用internalClone 。intervalClone 是一个 native 方法,一般来说 native 方法的执行效率高于非 native 方法。

浅拷贝和深拷贝的区别

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型,数组或引用),拷贝的就是内存地址。因此如果其中一个对象改变了这个地址,就会影响到另一个对象。Object 的 clone() 方法,提供的是一种浅克隆的机制。而拷贝构造方法指的是该类的构造方法参数为该类的对象。浅拷贝的实现方式如下:

  • 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据
  • 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。如果想要实现对对象的深克隆,在不引入第三方jar包的情况下,可以使用如下办法:

  • 先对对象进行序列化,紧接着马上反序列化出
  • 重写 clone 方法。与通过重写 clone 方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现 Cloneable 接口并重写 clone 方法。最后在最顶层的类的重写的 clone 方法中调用所有的 clone 方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝
  • 拷贝构造函数

数组的复制方法

  • for循环
  • Arrays.copyOf 本质上是调用 System.arraycopy 。之所以时间差距比较大,是因为很大一部分开销全花在了 Math.min 函数上了。所以,相比之下,System.arraycopy 效率要高一些。
  • clone() 比较特殊,对于对象而言,它是深拷贝,但是对于数组而言,它是浅拷贝。
  • 对于基本数据类型来说 System.arraycopy() 方法是深拷贝;对于引用数据类型来说 System.arraycopy() 方法是浅拷贝。System.arraycopy线程不安全!!!
public static byte[] copyOf(byte[] original, int newLength){
    byte[] copy = new byte[newLength];
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}
public static void arraycopy(
            Object src,  //源数组
            int srcPos,  //源数组的起始位置
            Object dest, //目标数组
            int destPos, //目标数组的起始位置
            int length   //复制长度
        )

数据类型

Java基本数据类型

在Java***有8种基本数据类型,分别是:

  • byte:1字节,范围为-128-127
  • short:2字节,范围为-32768-32767
  • int:4字节,范围为正负21亿
  • long:8字节
  • float:4字节
  • double:8字节
  • booolean:比较特殊,当作为单变量是与int相等;当作为数组时占用1字节
  • char:2字节

为什么char类型是双字节

JVM中内码采用UTF16。早期,UTF16采用固定长度2字节的方式编码,两个字节可以表示65536种符号(其实真正能表示要比这个少),足以表示当时unicode中所有字符。但是随着unicode中字符的增加,2个字节无法表示所有的字符,UTF16采用了2字节或4字节的方式来完成编码。Java为应对这种情况,考虑到向前兼容的要求,Java用一对char来表示那些需要4字节的字符。所以,java中的char是占用两个字节,只不过有些字符需要两个char来表示。

值得注意的是char类型,char在内存中采用UTF-16编码,一些字符需要使用两个char表示,故1个字符可能需要2-4个字节表示。在UTF-8中,英文字符占用1个字节,大部分汉字占用3个字节,少量占用4字节;而在UTF-16中,英文字符占用2个字节,大多数汉字占用2字节,个别汉字占用4个字节

下列操作是否合法

byte b1 = 10;
byte b2 = 11;
byte b3 = b1 + b2;// 均不合法,右操作数类型默认是int型,不进行强转会损失精度
byte b3 = (byte)(b1 + b2);// 正确
short s1 = 1; 
s1 = s1 + 1;//均不合法,右操作数类型默认是int型,从int转换到short可能会有损失

Java中包装类及装箱、拆箱

虽然 Java 语言是典型的面向对象编程语言,但其中的八种基本数据类型并不支持面向对象编程,基本类型的数据不具备“对象”的特性——不携带属性、没有方法可调用。沿用它们只是为了迎合人类根深蒂固的习惯,并的确能简单、有效地进行常规数据处理。为解决此类问题,Java为每种基本数据类型分别设计了对应的类,称之为包装类。每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的方法。包装类对象一经创建,其内容(所封装的基本类型数据值)不可改变。

下面代码描述了自动装箱的过程:

Integer i3 = 1;// 当把一个int型赋给一个Integer型时,引发编译器自动装箱,转化为Integer i3 = new Integer(1);

下面代码描述了自动拆箱的过程:

i4 = new Integer(1);// 由于基本类型无法指定对象,所以编译器会自动拆箱,该条语句会转换为:int i4  = new Integer(1).intvalue();

Java中包装类的常见误区

public void testEquals() {
        int int1 = 12;
        int int2 = 12;
        Integer integer1 = new Integer(12);
        Integer integer2 = new Integer(12);
        Integer integer3 = new Integer(127);
        Integer a1 = 127;
        Integer a2 = 127;
        Integer a = 128;
        Integer b = 128;
        System.out.println("int1 == int2 -> " + (int1 == int2));    // true        
        System.out.println("int1 == integer1 -> " + (int1 == integer1));    // true        
        System.out.println("integer1 == integer2 -> " + (integer1 == integer2));        // false
        System.out.println("integer3 == a1 -> " + (integer3 == a1));  // false
        System.out.println("a1 == a2 -> " + (a1 == a2));       // true        
        System.out.println("a == b -> " + (a == b));        // false                            
    }
  • 1.略
  • 2.Integer是int的封装类,当Integer与int进行==比较时,Integer就会拆箱成一个int类型,所以还是相当于两个int类型进行比较
  • 3.两个都是对象类型,而且不会进行拆箱比较
  • 4.integer3是一个对象类型,而a1是一个常量它们存放内存的位置不一样,所以也不等
  • 5.128不在缓存范围内,所以会new出两个不同的对象

隐式类型转换和显示类型转换

  • 当将占位数少的类型赋值给占位数多的类型时,java自动使用隐式类型转换(如int型转为long型)
  • 当把在级别高的变量的值赋给级别低变量时,必须使用显式类型转换运算(如double型转为float型)

包装类中的缓存

包装类存在缓存池设计,当使用Integer.valueOf(123)时,会尝试从缓存池中取对象(如果存在的话),多次调用会取得同一个对象的引用;而new Integer(123)则会新建一个对象,下面是常见的比较:

Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

Integer中valueOf方法的实现

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

valueOf方法首先判断值是否在缓存池中,如果在的话就直接返回缓存池中的内容。在 JDK8 中,Integer 缓存池可缓存的数值范围为-128 到 127。所以,在申请一个 [-128,127] 内的数时,其实是从 cache 中直接取出来用的,如果不在这个范围则是 new 了一个 Integer 对象。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。值得注意的是这个上界是可以通过 JVM 参数修改。

字符型常量和字符串常量的区别

  • 从形式上来看,字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干个字符
  • 从含义上来看,字符常量相当于一个整型值即ASCII值,可以参加表达式运算;字符串常量代表一个地址值,即该字符串在内存中存放位置
  • 从占用空间上看,字符常量只占2字节;字符串常量占若干个字节

编码

Java字符、字节与编码

  • 位(bit):是计算机内部数据储存的最小单位,11001100是一个八位二进制数
    字节(byte):是计算机中数据处理的基本单位,习惯上用大写B来表示,1B(byte,字节)=8bit(位)
  • 字符:是指计算机中使用的字母、数字、字和符号
  • ASCIIS码:1个英文字母(不分大小写)=1个字节的空间;1个中文汉字=2个字节的空间;1个ASCII码=一个字节.总共有 128 个,用一个字节的低7位表示,031是控制字符如换行回车删除等;32126 是打印字符.
  • GB2312:全称是《信息交换用汉字编码字符集基本集》,它是双字节编码,总的编码范围是A1-F7,其中从A1-A9是符号区,总共包含682个符号,从B0-F7是汉字区,包含 6763个汉字
  • GBK:全称叫《汉字内码扩展规范》,是国家技术监督局为windows95所制定的新的汉字内码规范,它的出现是为了扩展GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有23940个码位,它能表示21003个汉字,它的编码是和GB2312兼容的,也就是说用GB2312编码的汉字可以用GBK来解码,并且不会有乱码
  • UTF-8编码:1个英文字符=1个字节;英文标点=1个字节;1个中文(含繁体)=3个字节;中文标点=3个字节
  • UTF-16编码:个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)
  • UTF-32编码:世界上任何字符的存储都需要4个字节
  • Unicode编码:1个英文字符=2个字节;英文标点=2个字节;1个中文(含繁体)=2个字节;中文标点=2个字节

为什么需要编码

计算机中存储信息的最小单元是一个字节即8个bit,所以能表示的字符范围是0~255 个,人类要表示的符号太多,无法用一个字节来完全表示,要解决这个矛盾必须需要一个新的数据结构char,从char到byte必须编码。

内码和外码的含义与区别

Java在内部都是unicode编码,简单来说,内码是char或String在内存里使用的编码方式;外码是除了内码以外的所有...(包括class文件的编码)。在Java中,使用String类的getBytes方法可以将内码转换成外码。

GBK和GB2312的区别

GB2312与GBK编码规则类似,但是GBK范围更大,它能处理所有汉字字符。所以GB2312与GBK比较应该选择GBK

UTF-8、UTF-16和UTF-32对比

UTF-16与UTF-8都是处理Unicode的编码方式。它们的编码规则不太相同,相对来说UTF-16编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如Java的内存编码就是采用UTF-16编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复。想比较而言UTF-8更适合网络传输,对ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于GBK和UTF-16之间。所以UTF-8在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。

UTF-8、UTF-16和UTF-32的应用场景

  • UTF-8最适合用来作为字符串网络传输的编码格式
  • UTF-16最适合当作本地字符串编码格式
  • UTF-32所有字符都是4字节,但是对英文为主的字符串来说,消耗空间太大没什么特殊癖好或者需求的话,暂时还用不上

简述码点和代码单元

  • 码点是指一个编码表中的某个字符对应的代码值。Unicode的码点分为17个代码级别,第一个级别是基本的多语言级别,码点从U+0000——U+FFFF,其余的16个级别从U+10000——U+10FFFF,其中包括一些辅助字符。
  • 代码单元,Java字符串由Char值序列组成,Char的数据类型是一个采用Unicode码点的代码单元,即:Char数据类型是一个代码单元。任意Unicode字符都是一个码点,大多数常用的Unicode码点由一个char代码单元组成,辅助字符码点由两个char代码单元组成。

String类

在 JDK8 中,String 类使用 char[] 数组保存值。这个 char 数组使用 final 修饰符修饰,意味着一旦确定就引用即不可修改;此外,String 类中也没有提供修改数组修改值的方法。上述两种方式保证了 String 值的不可变性。此外 String 类只用 final 修饰,表明其不可继承。

String不可变性带来的好处

  • 可作为 Hash 的 key,减少计算量
  • 可以用于字符串缓存池
  • 具有安全性
  • 天生的线程安全性

String中equals方法

    public boolean equals(Object anObject) {
        // 首先使用==判断二者内存是否指向同一个地方
        if (this == anObject) {
            return true;
        }
        // 再判断类型是否都为String
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            // 如果类型相等再根据长度逐一对比
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                // 最后逐一对比字符是否相等
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

String、StringBuilder与StringBuffer的异同

StringBuilder 和 StringBuffer 均继承自 AbstractStringBuilder ,这个抽象父类里实现了除toString 以外的所有方法。StringBuilder 在重写方法的基础上没做其他扩展。StringBuffer 在重写方法的同时几乎为所有方法添加了synchronized 关键字用于同步。下面对这三者进行对比:

  • 可变性:String使用私有的常量char数组,因此不可变;其他二者均使用普通的char数组
  • 线程安全性上:String由于不可变性天生线程安全;StringBuffer则由于使用了synchronized关键字同样线程安全;StringBuilder则不保证线程安全
  • 性能:StringBuilder > StringBuffer > String
  • 是否可继承:三者均不可继承
  • equals方法:StringBuilder和StringBuffer均未重写该方法
  • valueOf方法:StringBuilder和StringBuffer均没有该方法
  • substring方法:String与StringBuffer中有该方法;StringBuilder中没有
  • toString方法:String直接返回自身;StringBuilder返回一个新的String方法;StringBuffer添加了synchronized方法

字符串常量池

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。当一个字符串调用 intern() 方法时,如果 StringPool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 StringPool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

字符串拼接优化

  • 字符串拼接从 JDK5 开始就已经完成优化,并且没有进行新的优化
  • 循环内 String + 常量 会每次 new 一个 StringBuilder ,再调用 append 方法
  • 循环外字符串拼接可以直接使用 String 的 + 操作,没有必要通过 StringBuilder 进行 append
  • 有循环体的话,好的做法是在循环外声明 StringBuilder 对象,在循环内进行手动 append。不论循环多少层都只有一个 StringBuilder 对象

StringBuffer 和 StringBuilder 的扩容策略:当字符串缓冲区容量不足时,原有容量将会加倍,以新的容量来申请内存空间,建立新的char数组,然后将原数组中的内容复制到这个新的数组当中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够预先评估 StringBuilder 或 StringBuffer 的大小,将能够有效的节省这些操作,从而提高系统的性能。

关键字 修饰符 特殊运算符

static关键字

static 关键字在 Java 中可修饰变量、方法、语句块和内部类(只能用于内部类,无法作为顶层类),下面将一一简介:

  • 静态变量又称类变量,该变量属于类,类的所有实例共享一份,在内存中也仅存一份
  • 静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。静态方法中不能使用 this 和 super
  • 静态语句块在类初始化时运行一次
  • 非静态内部类依赖于外部类的实例,而静态内部类不需要。此外,静态内部类不能访问外部类的非静态变量和方法
  • static 关键字与 final 一起用于定义常量

静态类的使用场景:

  • 当A类需要使用B类,并且B某类仅为A类服务,那么就没有必要单独写一个B类,因为B类在其他类中不会使用,所以只需将B类作为A的内部类。例如 ThreadLocal 与 ThreadLocalMap
  • 当某个类需要接受多个参数进行初始化时,推荐使用静态类构建,例如:
public class Car {
    private String name;
    private String model;
    private int height;
    private int width;

    private Car(Builder build){
        this.name=build.name;
        this.model= build.model;
        this.height=build.height;
        this.width=build.width;
    }
    public static class Builder {
        private String name;
        private String model;
        private int height;
        private int width;
        public Builder(){

        }
        public Builder withName(String name){
            this.name=name;
            return this;
        }
        public Builder withModel(String model){
            this.model=model;
            return this;
        }
        public Builder withHeight(int height){
            this.height=height;
            return this;
        }
        public Builder withWidth(int width){
            this.width=width;
            return this;
        }
        public Car build(){
            return new Car(this);
        }
    }
}

父子类的初始化顺序

优先级如下:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

内部类与static关键字的关系

普通内部类,没有 static 修饰,创建这个内部类时必须先创造外部类,即内部类依赖于外部类。此外,内部类对象可以访问外部类中的所有访问权限字段;外部类对象也可以通过内部类的对象引用访问内部类中定义的所有访问权限的字段。

静态内部类,使用 static 修饰,创建这个内部类时无需先创在外部类,即内部类不依赖于外部类。静态内部类无法访问外部类的非静态成员,因为外部类的非静态成员属于每一个外部类对象。外部类可以访问静态内部类对象的所有访问权限的成员。

内部类详细介绍

final关键字

final 用于声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 作用于基本类型变量,final 使数值不变
  • 作用于引用类型变量,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的例如数组
  • 作用于方法,final 使得该方法不能被重写。被 final 修饰的方法运行速度快于非 final 方法,因为在编译时已经被静态绑定,而无需运行时动态绑定。private 方法则被隐式地指定为 final
  • 作用于类,final 使得该类无法被继承,例如 String,Integer
  • final 与 abstract具有反相关关系

使用final关键字的好处:

  • final 关键字提高了性能,JVM 和 Java 应用都会缓存 final 变量
  • final 变量可以安全地在多线程环境下运行,无需同步开销
  • JVM 会对 final 方法、变量、类进行优化

static与final修饰变量所处JVM中的位置

class Fruit {
    static int x = 10;    // static int x 在方法区
    static BigWaterMelon bigWaterMelon_1 = new BigWaterMelon(x);    // static BigWaterMelon bigWaterMelon_1在方法区,而new BigWaterMelon(x)在堆上

    int y = 20;        // int y=20 在堆上
    BigWaterMelon bigWaterMelon_2 = new BigWaterMelon(y);    // BigWaterMelon bigWaterMelon_2 与 new BigWaterMelon(y) 都在堆上

    public static void main(String[] args) {    // String[] args 在vm栈上
        final Fruit fruit = new Fruit();        // Fruit fruit 在vm栈上,而 new Fruit() 在堆上

        int z = 30;    // int z = 30 在vm栈上
        BigWaterMelon bigWaterMelon_3 = new BigWaterMelon(z);    // BigWaterMelon bigWaterMelon_3 在vm栈上,而new BigWaterMelon(z)在堆上

        new Thread() {
            @Override
            public void run() {   
                int k = 100;    // int k=100 在栈帧上
                setWeight(k);
            }

            void setWeight(int waterMelonWeight) {    // int waterMelonWeight 在栈帧上
                fruit.bigWaterMelon_2.weight = waterMelonWeight;
            }
        }.start();
    }
}

class BigWaterMelon {
    public BigWaterMelon(int weight) {
        this.weight = weight;
    }

    public int weight;
}

图片说明

native

native 用来修饰方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。简单地讲,一个 native Method 就是一个 Java 调用非 Java 代码的接口。

instanceof

instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:boolean result = obj instanceof Class;其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果 result 都返回 true ,否则返回 false 。编译器会检查 obj 是否能转换成右边的 class 类型,如果不能转换则直接报错;如果不能确定类型,则通过编译,具体看运行时定。注意:编译器会检查 obj 是否能转换成右边的class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。

System.out.println(null instanceof Object);       // 输出false

ArrayList arrayList = new ArrayList();
System.out.println(arrayList instanceof List);    // 输出true
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

instanceof详解

super

可以使用 super() 函数访问父类的构造函数,从而委托父类进行一些初始化工作;可以使用 super.变量名 或 super.函数名 访问父类的变量或调用函数。super 与 this 的区别如下:

  • super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句);this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)
  • super 引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 或 super.成员函数名();this 代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)
  • 调用 super() 必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
  • super() 和 this()类似,区别在于 super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。
  • super() 和 this()均需放在构造方法内第一行。
  • 尽管可以用 this调用一个构造器,但却不能调用两个。
  • this 和 super 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。
  • 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个Java关键字。

包访问权限控制符

四种访问控制符具有不同级别的访问权限:

  • 如果一个成员需要被外部包所访问,则必须使用 public 修饰符
  • 如果一个成员需要被本包下的其他类所访问,则可以不用写任何的修饰符,使用 public 或者 protected 也行;若一个成员想使用同类中其他成员,则使用任意一个修饰符即可
  • 若一个成员不想被任何一个外部的类所访问,则使用 private 关键字比较恰当
  • default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问
  • protected 即使子类在不同的包中也可以访问

值得注意的是 Java 的访问控制是停留在编译层的,也就是它不会在 .class 文件中留下任何的痕迹,只在编译的时候进行访问控制的检查。其实,通过反射的手段,是可以访问任何包下任何类中的成员,例如,访问类的私有成员也是可能的。所以访问控制符有时是无效的。

图片说明

包访问测试实例

面向对象

面向对象三大特征

类是面向对象中一个重要的概念。类是具有相同属性和行为特征的对象的抽象,类是对象的概念模型,对象是类的一个实例,通过类来创建对象,同一类的所有对象具有相同的属性和行为特征。类具有三个基本特征:封装、继承、多态。

  • 封装就是将对象的属性和行为特征包装到一个程序单元(即类)中,把实现细节隐藏起来,通过公用的方法来展现类对外提供的功能,提高了类的内聚性,降低了对象之间的耦合性。

  • 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

  • 多态是建立在继承的基础上的,是指子类类型的对象可以赋值给父类类型的引用变量,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。

    继承相关难题

简述重写和重载的区别

重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。重写有三个限制:子类方法的访问权限必须大于等于父类方法;子类方法的返回类型必须是父类方法返回类型或为其子类型;子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。

重载存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。

构造器可以重写么

父类的构造器和私有属性不能被继承,所以无法重写,但是构造器其本身可以进行重载,即一个类里有多个构造函数的情况

抽象类的相关特性

  • 被 Abstract 关键字修饰
  • 含有抽象方法的类一定是抽象类,但是抽象类不一定含有抽象方法;且抽象方法必须是 public 或protected,否则不能被子类继承。默认修饰符根据 Java 版本而定
  • 抽象方法可以有具体数据和具体实现!(亲测通过)
  • 抽象类中可以定义自己的成员变量权限没要求,private,protected,public(亲测通过)
  • 抽象类中的抽象方法要被实现,所以不能用 static 和 private 修饰。
  • 子类继承抽象类时,必须实现抽象类中的所有抽象方法,否则该子类也要被定义为抽象类
  • 抽象类不能被实例化,只能引用其非抽象子类的对象
  • 可以有构造器,初始化块,内部类
  • Abstract 不能修饰成员,局部变量,

接口的相关特性

  • 接口中变量类型默认且只能是 public staic final
  • 接口中声明抽象方法,且只能是默认的public abstract,没有具体的实现
    默认的方法没有方法体,但JDK1.8之后有默认方法,静态方法是要有方法体
  • 子类必须实现所有接口函数
  • 不能定义构造器和初始化块
  • 接口可多继承
  • 接口的实现类必须全部实现接口中的方法,如果不实现,可以将子类变成一个抽象类

接口和抽象类的区别

  • 是否可多继承:抽象类不可多继承;接口可以
  • 是否有构造器:抽象类可以由构造器;接口不可以
  • 是否可用静态代码块和静态方法:抽象类可以;接口不可以
  • 实现方式:extends;implements
  • 接口是对动作的抽象;抽象类是对根源抽象

从实现方式;多继承;是否有构造器;是否有静态块;是否有静态方法;默认变量修饰符;默认方法修饰符;是否有默认实现;抽象方法必须全部实现?;是否可实例化等方面回答。

接口与抽象类在不同版本中的变化

  • 抽象类在1.8以前,其方法的默认访问权限为protected;1.8后改为default
  • 接口在1.8以前,方法必须是public;1.8时可以使用default;1.9时可以是private

通过实例对象.方法名这种调用过程的流程

  • 编译器查看对象的声明类型和方法名;如果有多个同名但参数类型不同的函数,那么编译器将一一列举所有该类中同名的方法和超类中同名的方法
  • 编译器查看调用方法时提供的参数类型。如果存在一个参数匹配的方法,那么就使用这个方法,这个过程称之为重载解析;如果未找到一个匹配的参数,那么就会报错
  • 如果该方法是 private 方法、 static 方法、 final 方法或者构造器,则编译器可以准确地知道应该调用哪种方法,被称之为静态绑定。与之对应的是,调用方法依赖于隐式参数的实际类型,并在运行时实现动态绑定
  • 当程序运行时并采用动态绑定调用方法时,虚拟机一定会调用最合适的那个类方法,否则层层向超类上搜索

值得注意的是,在调用方法搜索时,时间开销相当大。因此 ,虚拟机为每个类预先创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。通过查表节省时间

为什么在父类中要定义一个没有参数的空构造函数

Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

public class Main {
    public static void main(String[] args) {
        Parent dp = new Son(12);
    }
}
class Parent {
    int age;
    // 注释无参构造函数,无法通过编译
    Parent() {}
    Parent(int age) {
        this.age = age;
    }
}
class Son extends Parent {
    int height;
    Son(int height) {
        super();
        this.height = height;
    }
}

简述静态类和单例的区别

  • 单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员)
  • 单例可以被延迟初始化,静态类一般在第一次加载是初始化
  • 单例类可以被集成,他的方法可以被覆写
  • 单例类可以被用于多态而无需强迫用户只假定唯一的实例
  • (静态方法)静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果是用singleton,产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出

成员变量与局部变量

  • 成员变量可以使用 public,private,static等修饰符修饰;而局部变量不能被访问修饰符以及static 修饰。二者均可被final修饰
  • 成员变量在堆中,而局部变量在栈中
  • 成员变量如果未被赋初值,则会自动以默认值赋值,final修饰则需要显式地手动赋值;而局部变量不会被自动赋初值,直接拿来用会抛异常

静态方法和实例方法的区别

  • 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

为什么Java中只有值传递

在程序设计语言中存在两种传递方式,分别是传值调用和传址调用。传值调用指的是方法接收的是调用者提供的值,而传址调用指的是调用者提供变量的地址。一个方法可以修改传址调用所对应的变量值,而不能修改传值调用所对应的变量值。

Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。所以得到如下结论:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

常量池

简述Java中的字符串常量池

在 HotSpotVM 里实现的 string-pool 功能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是 1009 ;这个 StringTable 在每个 HotSpot-VM 的实例中只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了 StringTable 上。在 JDK6 中,StringTable 的长度是固定的,长度就是 1009,因此如果放入 String Pool 中的 String 非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降。在 JDK7 中,StringTable 的长度可以通过参数指定:-XX:StringTableSize=66666。

字符串常量池随JDK版本的位置变化

  • 7 以前方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动 JVM 时可以设置一个固定值,不可变
  • 7 时存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK7中,并没有完全移除,譬如符号引用(Symbols)转移到了 native memory;字符串常量池(interned strings)转移到了 Java heap;类的静态变量(class statics)转移到了 Java heap
  • 8 中取消永久代,其余的数据作为元数据存储在元空间中存放于元空间(Metaspace), 元空间是一块与堆不相连的本地内存。

Class常量池

Java 类被编译后,就会形成一份 class 文件; class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。每个 class 文件都会有一个 class 常量池。

运行时常量池

运行时常量池存在于内存中,也就是 class 常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

异常

什么是异常

如果某个方法无法按照正常的途径完成任务,则需要提供另外一种途径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法立即退出不返回任何值。此外,调用这个方法的其他代码也无法继续执行,异常处理机制将代码执行交给异常处理器。Throwable 是所有异常或错误的超类,其子类有 Error 和 Exception,Exception 的子类有 RuntimeException 和 IOException 。

常见的五个运行时异常:

  • NullPointerException(空指针)
  • ArithmeticException(运算)
  • ClassCastException(类转换异常)
  • ArrayIndexOutOfBoundsException(数组越界)
  • StringIndexOutOfBoundsException(字符串下标越界)

常见的五个编译时异常:

  • FileNotFoundException
  • ClassNotFoundException
  • SQLException
  • NoSuchFieldException
  • NoSuchMethodException

Exception和Error的区别

  • Error是指Java运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象
  • Exception分为两种,分别是RuntimeException(非受检异常)和CheckedException(受检异常)。其中 RuntimeException 是 JVM 在正常运行期间抛出的异常的超类,表明程序员出错;CheckedException 一般发生在编译期,Java 编译器会强制程序去捕获此类异常,并将这段可能出现异常的程序进行 try-catch

Throw和Throws的区别

  • throws 用于函数外,后面紧跟异常类,可以有多个异常类;throw 用在函数内,只能抛出一种异常类
  • throws 用于声明可能发生的异常,且该异常不一定发生;throw 则是抛出异常,一旦执行则必然发生异常
  • 二者遇到异常都仅仅抛出,而不是进行处理,真正的处理由函数的上层调用处理

泛型

简述你对泛型的理解

泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也即所操作的数据类型被指定为一个参数。

泛型可以用在如下地方:

  • 泛型方法:<? extends T>表示通配符所代表的类型是T类型的子类;<? super T>表示通配符所代表的的类型是 T 类型的超类
  • 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符

泛型擦除

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List<object> 和 List<string> 等类型,在编译之后都会变成List 。JVM 看到的只是 List ,而由泛型附加的类型信息对 JVM 来说是不可见的。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object 。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。</string></object>

反射

什么是反射

反射(Reflection)是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect类库主要包含了以下三个类:

  • Field
  • Method
  • Constructor

反射具有一些优点:

  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类
  • 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码
  • 调试器和测试工具:调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率

反射的一些缺点:

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射
  • 安全性:反射在一定程度上破坏了安全性
  • 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性

反射最重要的用途就是开发各种通用框架。很多框架(比如Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

枚举

简述你对枚举的理解

枚举是 JDK5 版本新增的特性(泛型、For-each等如今被广泛应用的特性也是由 JDK5 时所新增的),另外到了 JDK6 后 switch 语句支持枚举类型。枚举具有以下特征:

  • 使用关键字 enum
  • 枚举可以单独定义在一个文件中,也可以嵌在其它Java类中
  • 枚举可以实现一个或多个接口(Interface)
  • 枚举可以定义新的变量
  • 枚举可以定义新的方法
  • 枚举可以定义根据具体枚举值而相异的类
  • 枚举枚举不可被继承

枚举与常量类的区别

  • 常量类编译时,是直接把常量的值编译到类的二进制代码里,常量的值在升级中变化后,需要重新编译引用常量的类,因为里面存的是旧值。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类
  • 枚举类编译后默认为 final-class ,不允许继承可防止被子类修改。常量类可被继承修改、增加字段等,容易导致父类的不兼容
  • 当使用常量类时,往往通过 equals 去判断两者是否相等。枚举由于常量值地址唯一,可以用==直接对比,性能会有提高
  • switch 语句支持枚举型,当 switch 使用 int 、 String 类型时,由于值的不稳定性往往会有越界的现象,对于这个的处理往往只能通过 if 条件筛选以及 default 模块来处理。而使用枚举型后,在编译期间限定类型,不允许发生越界的情况

枚举的本质

枚举类在编译后会生成一个新的final class,这个类继承于 java.lang.Enum 。当一个 Java 类第一次被使用时,静态资源会被初始化,而 Java 类的加载以及初始化过程都是线程安全的,所以创建一个枚举类是线程安全的。枚举类在用于单例模式时可以有效地执行单例。

枚举与序列化的关系

在使用单例模式时,绝大多数单例的实现方式一旦实现了 Serializable 接口后,由于反射的存在,单例特性被破坏,每次调用 readObject 方法返回的都是一个新创建的对象。Java 对枚举做了特殊规定,使得每一个枚举类型记忆定义的枚举变量在 JVM 中都是唯一的。在序列化的时候 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

序列化

什么是序列化

序列化是一种对象持久化的手段。普遍应用在网络传输的场景中。对于 Java 而言,序列化主要用于对对象进行持久化操作。一般情况下,Java 对象的生命周期短于 JVM 的生命周期,随着 JVM 停止运行,Java 对象也随之消亡。但是,为了能在 JVM 停止运行后对指定对象进行持久化,并在将来的某个时刻重新读取被保存的对象,或跨 JVM 传输对象,持久化能实现上述需求。Java 在持久化对象时,会将对象的状态保存为一组字节。在反序列化时,将字节组装成对象。值得注意的是,序列化保存的的是对象的状态,即它的成员变量,而不会保存类的静态变量和被transient修饰的变量。

Java序列化的相关特性

  • 只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化
  • 通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
  • 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
  • 序列化并不保存静态变量
  • 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口
  • Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null
  • 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全

Java序列化的方式

  • 实现空接口 Serializable:这种方式隐式实现序列化,是最简单的序列化方式,自动序列化所有非statictransient修饰的成员变量
  • 实现 Externalizable 接口:这种方式必须实现 writeExternal() 和 readExternal() 方法,而且只能通过手动进行序列化,并且两个方法是自动调用的。因此,这个序列化过程是可控的,可以自己选择哪些部分序列化。
  • 实现 Serializable 接口并添加 writeObject() 和 readObject() 方法

第三种方式需要额外注意一下,writeObject() 和 readObject() 方法是被添加的,而不是重写或覆盖。在添加时,有格式要求。首先,这两个方法必须被 private 修饰;其次,第一行应该默认调用 defaultReadObject() 与 defaultWriteObject() 方法,目的是隐式序列化非static非transient变量;最后调用 readObject() 和 writeObject() 进行显示序列化。例如:

public class SerDemo implements Serializable{
    public transient int age = 23;
    public String name ;
    public SerDemo(){
      System.out.println("默认构造器。。。");
    }
    public SerDemo(String name) {
      this.name = name;
    }
    private  void writeObject(ObjectOutputStream stream) throws IOException {
     stream.defaultWriteObject();
        stream.writeInt(age);
    }
    private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
        stream.defaultReadObject();
     age = stream.readInt();
    }
    public String toString() {
        return "年龄" + age + "  " + name; 
   }

ArrayList中的序列化

首先,ArrayList 中实现了 Serializable 接口,意味着这个集合类可以进行序列化操作。在ArrayList 底层,保存具体集合元素的 Object[] 被 transient 修饰,但是集合元素经过反序列化后依旧可以复原。这是因为在 ArrayList 中定义了 writeObject 和 readObject 方法。在序列化过程中,虚拟机如果未发现用户定义的 writeObject 和 readObject 方法调用 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。如果用户定义了 writeObject 和 readObject 方法,则会按照用户的需求,调用自定义序列化与反序列化逻辑。 ArrayList 由于其动态性,数组容量不等于数组实际数量,当序列化时可能会将大量空值序列化为 null ,造成不必要的浪费,因此 ArrayList 定义了writeObject 和 readObject 方法使得数组元素被序列化进二进制流。在实际的应用中,序列化时将 Object[] 中下标 [0, size-1] 的数据逐一写入;反序列化时先读出数组长度,然后调用ensureCapacityInternal 方法扩容,最后逐一读出。

Java序列化背后的原理

Java 序列化可以通过实现 Serializable 接口,并通过添加 writeObject 和 readObject 方法达成。这两个方法并不是通过显示调用,而是反射进行调用,ArrayList 中就是这么实现的。以 ObjectOutputStream 的 writeObject 方法为例,其调用过程如下:

  • writeObject
  • writeObject0
  • writeOrdinaryObject
  • writeSerialData
  • invokeWriteObject
    值得注意的是在 writeObject0 类中有一段判断序列化类的类型是否是 Enum、 Array 和 Serializable 类型,如果不是则直接抛出 NotSerializableException 。代码如下:
    if (obj instanceof String) {
          writeString((String) obj, unshared);
      } else if (cl.isArray()) {
          writeArray(obj, desc, unshared);
      } else if (obj instanceof Enum) {
          writeEnum((Enum<?>) obj, desc, unshared);
      } else if (obj instanceof Serializable) {
          writeOrdinaryObject(obj, desc, unshared);
      } else {
          if (extendedDebugInfo) {
              throw new NotSerializableException(
                  cl.getName() + "\n" + debugInfoStack.toString());
          } else {
              throw new NotSerializableException(cl.getName());
          }
      }
    }

Java序列化中的serialVersionUID

serialVersionUID 用于 Java 的序列化机制。简单来说,Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException 。序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会去检测文件中的 serialVersionUID ,判断它是否与当前类的 serialVersionUID 一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。serialVersionUID 有两种生成机制:

  • 采用默认的private static final long serialVersionUID = 1L;
  • 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
    private static final long serialVersionUID = xxxxL;

当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用。这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。

Java序列化中父类相关问题

一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。

代理

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式,即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能同时又能起到隔离作用。这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

静态代理

Java 中的静态代理类似于装饰者模式,静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。

动态代理

动态,指的是代理类在程序运行时创建的,而不是在程序运行前手动编码来定义代理类的。这些动态代理类是在运行时候根据我们在 JAVA 代码中的“指示动态生成的。动态代理不需要实现接口,其核心实现方法在于利用 JDK 的 API ,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)。动态代理也被称为 JDK 代理,接口代理。JDK 实现代理只需要使用newProxyInstance方法。

public class ProxyFactory{
    //维护一个目标对象
    private Object target;
    public ProxyFactory(Object target){
        this.target=target;
    }
   //给目标对象生成代理对象
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("开始事务2");
                        //运用反射执行目标对象方法
                        Object returnValue = method.invoke(target, args);
                        System.out.println("提交事务2");
                        return returnValue;
                    }
                }
        );
    }
}

cglib

Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口。它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 synaop,为他们提供方法的 interception (拦截) Cglib 包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类。不鼓励直接使用 ASM ,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。

cglib和jdk的这两者之间性能的区别

在 1.6 和 1.7 的时候,JDK 动态代理的速度要比 CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距。在 JDK1.8 的时候,JDK 动态代理的速度已经比 CGLib 动态代理的速度快很多。

动态代理用多了之后对内存方面有什么影响

基于 cglib 的动态代码,此代理在设置用户缓存为 true 时不会产生内存溢出;设置为false时,会引发内存溢出。