学习笔记之装箱与拆箱、常量池、StringBuffer与StringBuilder、forEach原理

本文深入探讨Java中的装箱与拆箱机制,分析常量池的作用与分类,对比String、StringBuffer与StringBuilder的线程安全性和运行速度,以及forEach循环的内部实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

装箱与拆箱

所有的基本类型都有一个与之对应的类。例如,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虚拟机按照运行时内存使用区域划分如图:
jvm内存模型

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接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值