目录
非静态内部类能不能访问外部类所有访问权限修饰的字段(包括private),为什么?
String s=new String(“x”) 创建了几个对象?
String、StringBuffer 和 StringBuilder 区别?
那么为什么StringBuilder和StringBuffer效率高呢?
基本类型
Java 提供了八种基本数据类型格式,包括 4 种整型,2 种浮点,1 种字符(char 是两字节)以及 1 种布尔型类型。
引用数据类型:
类(class),接口,数组
类型有哪些转换方式 (强弱类型)
对于基本类型:主要分为自动(隐式)类型转换和强制(显式)类型转换两种方式。
自动类型转换的场景
1)小的类型自动转化为大的类型;
2)整数类型可以自动转化为浮点类型,可能会产生舍入误差;
3)字符可以自动提升为整数。
强类型转换场景:
1)强制类型转换可能导致溢出或损失精度;;
2)浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入;
3)不能对布尔值进行转换;
4)不能把对象类型转换为不相干的类型。
自动拆箱装箱
场景:
Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)——>使得基本数据类型也具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作
既然出现了基本类型以及对于的包装类,那么二者之间就会有转换操作
- Integer i =10; //自动装箱
- int b= i; //自动拆箱
自动拆装箱的原理
通过反编译可知,自动装箱都是通过valueOf()方法进行实现的;而拆箱是通过xxxValue()完成的
使用场景:
1)基本数据类型放入集合类;
2)包装类型和基本类型的大小比较;
3)包装类型的运算符计算;
4)函数参数与返回值。
性能上的提高(基本数据类的缓冲池)
由上文可知包装类以及自动拆装箱的好处,同时在开发中的大量应用,所以 Java 为了进一步提高性能和节省空间,整型 Integer 对象通过使用相同的对象引用实现了缓存和重用。
即当需要进行自动装箱时,在 -128 至 127 之间的整型数字会直接使用缓存中的对象,而不是重新创建一个新对象。当然,这个范围是可以通过 -XX:AutoBoxCacheMax=size 参数进行调整的。
Boolean类型占多少个字节
总结一下当前常见的说法:
1 bit
位是计算机最小的存储单位,且布尔类型的值只有 true 和 false,这两个数在内存中只需要1位(bit)即可存储,
1 byte
虽然 boolean 编译后只需占用 1 bit 空间,但计算机处理数据的最小单位是字节。
4 byte
在《Java 虚拟机规范》一书中说到:JVM 中没有任何可以供 boolean 值专用的字节码指令,Java 语言表达式所操作的 boolean 值,在编译之后都使用 int 类型来代替,而 boolean 数组将会被编码成 Java 虚拟机的 byte 数组,每个元素 boolean 元素占 8 位。
抽象类和接口
public abstract
修饰(jdk1.8可以用default和static开头),接口中的成员变量类型默认 public static final
。二者区别:
1)抽象类可以有非抽象方法(可以有普通方法),接口不存在非抽象方法;
2)使用 extends 继承抽象类并实现抽象方法,是 implements 实现接口中所有方法;
3)抽象类支持构造函数(但不能被 abstract 修饰),接口无构造函数;
4)抽象类的抽象方法可以被 public、protected、default 修饰,接口只能是 public;
5)抽象类可以有 main 函数,接口不支持;
另外,一个类只能继承一个抽象类,而一个类却可以实现多个接口
场景:
1.如果你想拥有一些方法并且让他们有一些默认实现,就可以用抽象类
2.多实现就是用接口,比如多个功能;Java不支持多继承
3.功能在不断改变可以使用抽象类
内部类
定义
内部类就是将自身类的定义放在另外一个类的定义内部的类。内部类本身就是类的一个属性,与其他属性定义方式一致
常见的内部类可以被分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。
静态内部类
静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)
但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员
这里直接用静态类对象调用里面的属性
- public class InnerClassTest {
- public int field1 = 1;
-
- public InnerClassTest() {
- System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
- // 创建静态内部类对象
- StaticClass innerObj = new StaticClass();
- System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);
- System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);
- System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);
- System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);
- }
-
- static class StaticClass {
-
- public int field1 = 1;
- protected int field2 = 2;
- int field3 = 3;
- private int field4 = 4;
- // 静态内部类中可以定义 static 属性
- static int field5 = 5;
-
- public StaticClass() {
- System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象");
- // System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!!
- }
- }
-
- public static void main(String[] args) {
- // 无需依赖外部类对象,直接创建内部类对象
- // InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();
- InnerClassTest outerObj = new InnerClassTest();
- }
- }
匿名内部类
匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:
思路:这里是有一个OnClickListener接口,我们在外层方法中通过匿名内部类重写接口方法,然后接口方法的参数需要一个对象——>这个对象再通过我们匿名内部类对象调用方法重写toString()得到一个参数obj值
- public class InnerClassTest {
-
- public int field1 = 1;
- protected int field2 = 2;
- int field3 = 3;
- private int field4 = 4;
-
- public InnerClassTest() {
- System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
- }
- // 自定义接口
- interface OnClickListener {
- void onClick(Object obj);
- }
-
- private void anonymousClassTest() {
- // 在这个过程中会新建一个匿名内部类对象,
- // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法
- OnClickListener clickListener = new OnClickListener() {
- // 可以在内部类中定义属性,但是只能在当前内部类中使用,
- // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,
- // 也就无法创建匿名内部类的对象
- int field = 1;
-
- @Override
- public void onClick(Object obj) {
- System.out.println("对象 " + obj + " 被点击");
- System.out.println("其外部类的 field1 字段的值为: " + field1);
- System.out.println("其外部类的 field2 字段的值为: " + field2);
- System.out.println("其外部类的 field3 字段的值为: " + field3);
- System.out.println("其外部类的 field4 字段的值为: " + field4);
- }
- };
- // new Object() 过程会新建一个匿名内部类,继承于 Object 类,
- // 并重写了 toString() 方法
- clickListener.onClick(new Object() {
- @Override
- public String toString() {
- System.out.println("创建参数对象");
- return "obj1";
- }
- });
- }
-
- public static void main(String[] args) {
- InnerClassTest outObj = new InnerClassTest();
- outObj.anonymousClassTest();
- }
- }
直接new一个接口,并实现这个接口声明的方法,这个时候就会创建一个匿名内部类来实现这个接口,并且重写接口声明的方法,并且起到赋值作用
注意:外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。
深入理解内部类
- public class InnerClassTest {
-
- int field1 = 1;
- private int field2 = 2;
-
- public InnerClassTest() {
- InnerClassA inner = new InnerClassA();
- int v = inner.x2;
- }
-
- public class InnerClassA {
- int x1 = field1;
- private int x2 = field2;
- }
- }
我们用javap -c 反编译一下class文件
们在这里发现了名为 access$000
的静态方法,并且这个静态方法接受一个 InnerClassTest$InnerClassA
类型的参数,方法的作用也很简单:返回参数代表的内部类对象的 x2
字段值
注意到编译器给内部类提供了一个接受 InnerClassTest
类型对象(即外部类对象)的构造方法,内部类本身还定义了一个名为 this$0
的 InnerClassTest
类型的引用,这个引用在构造方法中指向了参数所对应的外部类对象。
在 25 行字节码指令发现:内部类的构造方法通过 invokestatic 指令执行外部类的 access$100 静态方法(在 InnerClassTest 的字节码中已经介绍了)得到外部类对象的 field2 字段的值,并且在后面赋值给 x2 字段。这样的话内部类就成功的通过外部类提供的静态方法得到了对应外部类对象的 field2
是因为匿名内部类的类名被匿名了嘛,所以说外部类根部嗯拿不到,所以访问不了,而相反匿名内部类却可以范围跟外部类的私有成员,是因为外部类提供了静态方开获得对于外部类的私有成员
静态内部类不依靠其他外部类存在,所以静态内部类是得不到外部类的对象引用的(因为根本不需要)
到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。
内部类和内存泄漏
场景:
内存泄漏指的是——>在内存中存在一些其内存空间可以被回收的对象因为某些原因又没有被回收,因此产生了内存泄露,如果应用程序频繁发生内存泄露可能会产生很严重的后果(内存中可用的空间不足导致程序崩溃,甚至导致整个系统卡死)
在 Java 中,因为 JVM 有垃圾回收功能,对于我们自己创建的对象无需手动回收这些对象的内存空间
首先在之前,非静态内部类不是会得到我们外部类的引用嘛(通过反编译可知道)——>正是因为非静态内部类对象会持有外部类对象的引用,因此如果说这个非静态内部类对象因为某些原因无法被回收,就会导致这个外部类对象也无法被回收
- public class MemoryLeakTest {
-
- // 抽象类,模拟一些组件的基类
- abstract static class Component {
-
- final void create() {
- onCreate();
- }
-
- final void destroy() {
- onDestroy();
- }
-
- // 子类实现,模拟组件创建的过程
- abstract void onCreate();
-
- // 子类实现,模拟组件摧毁的过程
- abstract void onDestroy();
-
- }
-
- // 具体某个组件
- static class MyComponent extends Component {
- // 组件中窗口的单击事件监听器
- static OnClickListener clickListener;
- // 模拟组件中的窗口
- MyWindow myWindow;
-
- @Override
- void onCreate() {
- // 执行组件内一些资源初始化的代码
- clickListener = new OnClickListener() {
- @Override
- public void onClick(Object obj) {
- System.out.println("对象 " + obj + " 被单击");
- }
- };
- // 新建我的窗口对象,并设置其单击事件监听器
- myWindow = new MyWindow();
- myWindow.setClickListener(clickListener);
- }
-
- @Override
- void onDestroy() {
- // 执行组件内一些资源回收的代码
- myWindow.removeClickListener();
- }
- }
-
- // 我的窗口类,模拟一个可视化控件
- static class MyWindow {
- OnClickListener clickListener;
-
- // 设置当前控件的单击事件监听器
- void setClickListener(OnClickListener clickListener) {
- this.clickListener = clickListener;
- }
-
- // 移除当前控件的单击事件监听器
- void removeClickListener() {
- this.clickListener = null;
- }
-
- }
-
- // 对象的单击事件的监听接口
- public interface OnClickListener {
- void onClick(Object obj);
- }
-
- public static void main(String[] args) {
- MyComponent myComponent = new MyComponent();
- myComponent.create();
- myComponent.destroy();
- // myComponent 引用置为 null,排除它的干扰
- myComponent = null;
- // 调用 JVM 的垃圾回收动作,回收无用对象
- System.gc();
-
- System.out.println("");
- }
- }
问:如何避免内存泄漏?
1、能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;
2、对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。——>其实就是root引用,根本收不了
当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?
3、为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)。
内部类有哪些优点
修饰符
1.static,用来创建类方法和类变量。
2.final,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
3.abstract,用来创建抽象类和抽象方法。
4.synchronized 用于多线程的同步。
5.volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。——>保证可见性和有序性
6.transient:序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。
在 Java 中,final 关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
当一个类被 final 修饰时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用 final 进行修饰。
final 修饰的方法表示此方法已经是”最后的、最终的”含义,即此方法不能被重写(可以重载多个final修饰的方法)。
需要注意的一点是,如果父类中 final 修饰的方法同时访问控制权限为 private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数。因为此时没有产生重写,而是在子类中重新定义了新的方法。
当 final 修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果 final 修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。因此,被 final 修饰的成员变量必须要显示初始化。
Switch支持哪些数据类型
一般情况下使用整型类型,包括 byte、short、char 以及 int。但在 JDK 1.5 和 1.7 又分别增加了对枚举类型和 String 的支持。
Switch 对 String 类型的支持是利用 String 的 hash 值,本质上还是 switch-int 结构。并且利用了 equals 方法来防止 hash 冲突的问题。最后利用 switch-byte 结构,精确匹配。
- public static void main(String[] args) {
- switch (args[0]) {
- case "A" : break;
- case "B" : break;
- default :
- }
- }
-
- // 经过 JVM 编译后:
-
- public static void main(String[] var0) {
- String var1 = var0[0];
- byte var2 = -1;
- switch(var1.hashCode()) {
- case 65:
- if (var1.equals("A")) {
- var2 = 0;
- }
- break;
- case 66:
- if (var1.equals("B")) {
- var2 = 1;
- }
- }
-
- switch(var2) {
- case 0:
- case 1:
- default:
- }
- }
面向对象
在此之前,先来了解一下抽象的概念,抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装就是把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。——>场景:项目中注入接口调用实现类方法
在 Java 中有两种形式可以使用多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
重写(override):一般都是表示子类和父类之间的关系,其主要的特征是方法名相同,参数相同,但是具体的实现不同。
重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
主要区别:
1)重写发生在子类继承或接口实现类中,重载只发生在本类中;
2)二者均需要具有相同的方法名称;
3)重写的参数列表必须保持一致,重载的参数列表必须修改;
4)重写的返回参数必须保持一致,重载的参数列表可以修改;
5)重写的访问修饰符不能比父类中被重写的方法的访问权限更低,重载可以修改;
5)重写的异常可以减少或删除,但不能扩展,重载可以修改。
问:向前引用是什么?对应场景
所谓向前引用,就是在定义类、接口、方法、变量之前使用它们。
- class MyClass {
- int method() {return n; }
- int m = method();
- int n = 1;
- }
-
- // 如果简单地执行下面的代码,毫无疑问会输出1.
- System.out.println(new MyClass().method());
-
- // 使用下面的代码输出变量m,却得到0。
- System.out.println(new MyClass().m);
比如上面的代码中,n 在 method 方法后定义,但 method 方法中可以先使用该变量。
至于为何两次输出结果不同,这是因为当 Runtime 运行 MyClass.class 文件时,首先会进行装载成员字段,而且这种装载是按顺序执行的,并不会因为 Java 支持向前引用,就首先初始化所有值。
首先,Runtime 会初始化 m 字段,这时就会调用 method 方法,在 method 方法中利用向前引用技术使用了 n。Runtime 为了实现向前引用,在进行初始化所有字段之前,还需要将所有的字段添加到符号表中。以便在任何地方(但需要满足 Java 的调用规则)都可以引用这些字段,不过由于还没有初始化这些字段,所以这时符号表中所有的字段都使用默认的值
总的来说就是执行m时,n还没有被初始化
常见类库
标准API内容都是Java包发布的(比如IO,reflect,集合都在这里头),Javax相当于API的扩展,随着时间的推移,作为 javax 发布的扩展成为 Java API 的组成部分。但将扩展从 javax 包移动到 java 包太麻烦,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
clone() 创建斌返回此对象的副本;
equals() 判断;
getclass() 返回该对象的运行类;
hashcode() 返回对象的哈希码值;
notify() 唤醒正在等待对象监听器的线程;(虚假唤醒:唤醒但是由于条件变量又进入等待)
notifyAll() 唤醒正在等待对象监听器的所有线程;
wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()
或 notifyAll()
方法;
toString() 返回此对象的字符串表示形式;
finalize() 当垃圾收集确定不需要该对象时,垃圾回收器调用该方法。
一、对象类型不同
1、equals():是超类Object中的方法。
2、==:是操作符。
二、比较的对象不同
1、equals():equals是Object中的方法,在Object中equals方法实际"ruturn (this==obj)",用到的还是"==",说明如果对象不重写equals方法,实际该对象的equals和"=="作用是一样的,都是比较的地址值(因为"=="比较的就是地址值),但是大部分类都会重写父类的equals方法,用来检测两个对象是否相等,即两个对象的内容是否相等,例如String就重写了equals方法,用来比较两个字符串内容是否相同。
- 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;
- }
2、==:用于比较引用和比较基本数据类型时具有不同的功能,比较引用数据类型时,如果该对象没有重写equals方法——>则比较的是地址值; 如果重写了——>就按重写的规则来比较两个对象;基本数据类型只能用"=="比较两个值是否相同,不能用equals(因为基本数据类型不是类,不存在方法)。
==:
== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
1、比较的是操作符两端的操作数是否是同一个对象。(比较引用数据类型,重写equals情况)
2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:
int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。
equals:
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
String s="abce"是一种非常特殊的形式,和new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。(串池)
以String s="abce";形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"abcd"的对象。
如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个“abcd"”,下一次如果有Strings1="abcd";又会将s1指向“abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。
(46条消息) 牛客网刷题(垃圾回收+Socket+串池+类加载+事务)_Fairy要carry的博客-CSDN博客
hashCode()
是 Object 类的公共方法,返回一个哈希值。如果两个对象根据 equals()
方法比较相等,那么调用这两个对象中任意一个对象的 hashCode()
方法必须产生相同的哈希值
如果两个对象根据 eqauls()
方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的)。
String
1 个或 2 个都有可能。首先会创建一个 String 类型的变量 s。在类加载到此处之前没有出现 “x” 字面量的话,加载此处时还会额外创建一个对应 “x” 的 String 常量对象。在符合规范的JVM上,执行到此处 new 关键字时会创建。
可能说的不是很明白,众所周知,线程有原子性,可见性,有序性三种,而有序性是由JVM来维护的,可能出现无序的情况,我们这里讲讲JVM是如何执行这些的
String s="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向这个已经有的;如果常量池中不存在"abc",则在堆区new一个String对象,然后将"abc"放到常量池中,并将"abc"指向刚new好的String对象。(创建0或1个对象)
new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc"(2.1已经讲过了),然后再在堆区new一个String对象。(1或2个对象)
因此,现在可以解答本文的标题了,String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。
字符串常量池和运行时常量池是两个不同的概念。运行时常量池存储的是类的字面量,是每个类独有的,而字符串常量池存储的是字符串字面量,是所有类共享的。
JDK1.7字符串常量池在方法区,JDK1.7之后字符串常量池(串池)就转移到了堆区。
String是不可变的,用+操作符拼接字符串,会产生中间对象,如果是线程安全的环境,我们会用StringBuffer拼接字符串,线程不安全的环境则使用StringBuilder。
String类用声明为final的char数组来存储字符串,在拼接的时候由于char数组不可变,因此会产生中间对象;
那到底产生的是哪些中间对象呢?其实上面已经讲到了,+操作符拼接字符串时,每次会new一个StringBuilder对象,然后将+操作符连接的两个字符串append上,最后toString,而toString又会new一个String对象,因此每使用一次+操作符都会产生2个中间对象。
StringBuilder和StringBuffer都继承自AbstractStringBuilder,AbstractStringBuilder的源码如下图所示。
可以看到AbstractStringBuilder中的char数组没有声明为final,因此是可变的。