学习笔记之装箱与拆箱、常量池、StringBuffer与StringBuilder、forEach原理
装箱与拆箱
所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。常见的对象包装器类有:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前六个类派生于公共的超类Number)。
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。
简单来说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
自动装箱
装箱是通过调用包装器的valueOf方法实现的。
假设我们需要定义一个整型数组列表,而尖括号内不允许是基本类型:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从源码中可以看出我们需要用到Integer对象包装器类。我们可以声明一个Integer对象的数组列表。
ArrayList<Integer> list = new ArrayList<>();
但java具有一个很有用的特性,从而更加便于添加int类型的元素到ArrayList<Integer>中
list.add(3); //自动转换 -> list.add(Integer.valueOf(3));
add(3)将自动被转换为add(Integer.valueOf(3))
这种变换被称为自动装箱。
说明:
1)Integer.valueOf(int i) 表示返回一个指定的 int 值的 Integer 实例。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
2)由于每个值分别包装在对象中,所以ArrayList<Integer>的效率远低于int[ ]数组。因此更适合用于构造小型集合。原因是方便性比执行效率更重要。
自动拆箱
拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。
相反地,当将一个Integer对象赋给一个int值时,将会自动地拆箱。
int n = list.get(i); //自动转换 -> int n = list.get(i).intValue();
即将Integer类转换为int基本类型。
说明:
1)Integer.intvalue() 表示返回一个指定的Integer实例的 int值。
public int intValue() {
return value;
}
补充说明
关于自动装箱还有几点要说明:
1)可能会抛出NullPointerException:
Integer n = null;
System.out.println(2 * n);
由于包装器类引用可以为null,所以自动装箱有可能会抛出一个NullPointerException异常。
2)出现混合的包装器类型可能会自动拆箱:
如果在一个条件表达式中混合使用Integer和Double类型,Integer值会拆箱,提升为double,再装箱为Double。
Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x);
3)装箱和拆箱是编译器认可的,而不是虚拟机。
4)基本类型和包装类型的区别:
a)声明方式不同:
基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
b)存储方式及位置不同:
基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来
使用;
c)初始值不同:
基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
d)使用方式不同:
基本类型直接赋值直接使用就好,而包装类型在集合(如:Collection、Map)时会使用
到。
常量池
常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。
运行时常量池在内存模型中的位置
java虚拟机按照运行时内存使用区域划分如图:
1)程序计数器:
它的生命周期与线程相同,线程私有。较小的内存区域,用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。
2)虚拟机栈:
它的生命周期与线程相同,线程私有。虚拟机栈中存储了方法执行时相关信息,每个方法在调用时都会在虚拟机栈中创建一个方法帧,方法帧中包含了局部变量,参数,运行中间结果等信息。帧数超过限制(-Xss),就会出现StackOverFlow(=SOF)错误。另外超过线程分配的内存大小,也会报OOM错误。
3)本地方法栈:
它的生命周期与线程相同,线程私有。基本同虚拟机栈。存放的是native方法帧。可出现SOF和OOM错误。
4)元空间(MetaSpace):
所有线程共享。存放class加载相关信息。
5)堆:
所有线程共享。存放new出来的数组和对象数据,以及类的静态变量。同时,包含一个常量池(final),是由1.7以前版本的方法区转移过来的。
可见,运行时常量池存放于jvm的堆中。
常量池分类
常量池主要分为静态常量池和运行时常量池。
1)所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这部分内容将在类加载后进入运行时常量池中存放。
2)而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在堆中,我们常说的常量池,就是指堆中的运行时常量池。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
示例
String a1 = "a";
String a2 = "a";
System.out.println(a1 == a2);
String b1 = new String("b");
String b2 = new String("b");
System.out.println(b1 == b2);
结果为true,false。
分析
采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"a"这个对象,如果不存在,则在字符串池中创建"a"这个对象,然后将池中"a"这个对象的引用地址返回给"a"对象的引用a1,这样a1会指向池中"a"这个字符串对象;如果存在,则不创建任何对象,直接将池中"a"这个对象的地址返回,赋给引用a2。因为a1,a2都是指向同一个字符串池中的"a"对象,所以结果为true。
采用new关键字新建一个字符串对象时,JVM 并不会去检查对象是否存在,而是直接在堆中创建一个"b"字符串对象,然后将堆中的这个"b"对象的地址返回赋给引用b1,这样,b1就指向了堆中创建的这个"b"字符串对象;b2则指向了堆中创建的另一个"b"字符串对象。b1 、b2是两个指向不同对象的引用,结果当然是false。
String、String Buffer与String Builder
这三个类之间的区别主要是在两个方面,即线程安全和运行速度这两方面。
线程安全
在线程安全上,StringBuilder是线程不安全的,而String、StringBuffer是线程安全的。
1)String类:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
.....
}
可见String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。所有不可变类都是线程安全的。
2)StringBuffer类:
public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
.....
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
.....
}
StringBuffer的几乎所有方法都加了synchronized。
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer。
运行速度
StringBuilder > StringBuffer > String
String最慢的原因
浅层理解
由于String为final类,其对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。
所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,因此执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
深层原因
测试代码
public class Demo {
public static void main(String[] args) {
String src = "";
for (int i = 0; i < 10; i++) {
src = src + "A";
}
System.out.println(src);
}
}
打开cmd,使用命令:javap -c Demo.class
public class com.lamarsan.jvm.Demo {
public com.lamarsan.jvm.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 10
8: if_icmpge 37
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
22: ldc #6 // String A
24: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
27: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: astore_1
31: iinc 2, 1
34: goto 5
37: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
40: aload_1
41: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: return
}
可以发现,String在执行的过程中不断地new了StringBuilder对象,总共new了10次。
StringBuilder比StringBuffer快的原因
StringBuffer与StringBuilder都是继承自同一个抽象类,基本上方法都一样,实现也几乎都是一样的,而导致StringBuffer效率低一些的原因就是StringBuffer的几乎所有方法都加了锁。因为方法加了synchronized同步,效率便会稍慢一些。
总结
String:适用于少量的字符串操作的情况。
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。
forEach原理
forEach是Java的一种功能很强的循环结构,可以用来一次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标志而分心。
语句格式
for(variable : collection) statement
定义一个变量用于暂存集合中的每一个元素,并执行相应的语句或语句块。
编译器层面
forEach对集合遍历
原码:
public class Demo {
public static void main(String[] args) {
Integer[] arr = {1,2,3};
List<Integer> list = Arrays.asList(arr);
for (Integer i : list) {
System.out.println(i);
}
}
}
编译后:
public class Demo {
public Demo() {
}
public static void main(String[] args) {
Integer[] arr = new Integer[]{1, 2, 3};
List<Integer> list = Arrays.asList(arr);
Iterator var3 = list.iterator();
while(var3.hasNext()) {
Integer i = (Integer)var3.next();
System.out.println(i);
}
}
}
可以看见,编译器会先获得集合的iterator对象的实例,然后进行while循环,使用iterator的hasNext方法和next方法进行取值。因此,对于集合对象,编译器其实是靠Iterator迭代器来实现的。
forEach对数组遍历
对于数组,并没有实现Iterator接口,所以它是怎么遍历的呢?让我们来看看。
原码:
public class Demo {
public static void main(String[] args) {
Integer[] arr = {1,2,3};
for (Integer i : arr) {
System.out.println(i);
}
}
}
编译后:
public class Demo {
public Demo() {
}
public static void main(String[] args) {
Integer[] arr = new Integer[]{1, 2, 3};
Integer[] var2 = arr;
int var3 = arr.length;
for(int var4 = 0; var4 < var3; ++var4) {
Integer i = var2[var4];
System.out.println(i);
}
}
}
很明显,对于数组,编译器采用了for循环来遍历。
JVM层面
打开cmd,使用命令:javap -c Demo.class
public class com.lamarsan.jvm.Demo {
public com.lamarsan.jvm.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: anewarray #2 // class java/lang/Integer
4: dup
5: iconst_0
6: iconst_1
7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: aastore
11: dup
12: iconst_1
13: iconst_2
14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: dup
19: iconst_2
20: iconst_3
21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
24: aastore
25: astore_1
26: aload_1
27: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
30: astore_2
31: aload_2
32: invokeinterface #5, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
37: astore_3
38: aload_3
39: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
44: ifeq 69
47: aload_3
48: invokeinterface #7, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
53: checkcast #2 // class java/lang/Integer
56: astore 4
58: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
61: aload 4
63: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
66: goto 38
69: return
}
可以看到1-31行为创建了list集合,并赋值的过程,32行开始用Iterator开始遍历,所以本质上还是使用了Iterator接口。