你好,我是 Guide。秋招行将到来,我对 JavaGuide 的内容进行了重构欠缺,公众号同步一下最新更新,心愿可能帮忙你。
上篇:Java 根底常见知识点 & 面试题总结(上),2022 最新版!
原文地址:https://javaguide.cn/java/basis/java-basic-questions-02.html
面向对象根底
面向对象和面向过程的区别
两者的次要区别在于解决问题的形式不同:
- 面向过程把解决问题的过程拆成一个个办法,通过一个个办法的执行解决问题。
- 面向对象会先形象出对象,而后用对象执行办法的形式解决问题。
另外,面向对象开发的程序个别更易保护、易复用、易扩大。
相干 issue : 面向过程:面向过程性能比面向对象高??
成员变量与局部变量的区别
- 语法模式:从语法模式上看,成员变量是属于类的,而局部变量是在代码块或办法中定义的变量或是办法的参数;成员变量能够被
public
,private
,static
等修饰符所润饰,而局部变量不能被访问控制修饰符及static
所润饰;然而,成员变量和局部变量都能被final
所润饰。 - 存储形式:从变量在内存中的存储形式来看, 如果成员变量是应用
static
润饰的,那么这个成员变量是属于类的,如果没有应用static
润饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - 生存工夫:从变量在内存中的生存工夫上看,成员变量是对象的一部分,它随着对象的创立而存在,而局部变量随着办法的调用而主动生成,随着办法的调用完结而沦亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会主动以类型的默认值而赋值(一种状况例外: 被
final
润饰的成员变量也必须显式地赋值),而局部变量则不会主动赋值。
创立一个对象用什么运算符? 对象实体与对象援用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象援用指向对象实例(对象援用寄存在栈内存中)。
一个对象援用能够指向 0 个或 1 个对象(一根绳子能够不系气球,也能够系一个气球); 一个对象能够有 n 个援用指向它(能够用 n 条绳子系住一个气球)。
对象的相等和援用相等的区别
- 对象的相等个别比拟的是内存中寄存的内容是否相等。
- 援用相等个别比拟的是他们指向的内存地址是否相等。
类的构造方法的作用是什么?
构造方法是一种非凡的办法,次要作用是实现对象的初始化工作。
如果一个类没有申明构造方法,该程序能正确执行吗?
如果一个类没有申明构造方法,也能够执行!因为一个类即便没有申明构造方法也会有默认的不带参数的构造方法。如果咱们本人增加了类的构造方法(无论是否有参),Java 就不会再增加默认的无参数的构造方法了,咱们始终在人不知; 鬼不觉地应用构造方法,这也是为什么咱们在创建对象的时候前面要加一个括号(因为要调用无参的构造方法)。如果咱们重载了有参的构造方法,记得都要把无参的构造方法也写进去(无论是否用到),因为这能够帮忙咱们在创建对象的时候少踩坑。
构造方法有哪些特点?是否可被 override?
构造方法特点如下:
- 名字与类名雷同。
- 没有返回值,但不能用 void 申明构造函数。
- 生成类的对象时主动执行,无需调用。
构造方法不能被 override(重写), 然而能够 overload(重载), 所以你能够看到一个类中有多个构造函数的状况。
面向对象三大特色
封装
封装是指把一个对象的状态信息(也就是属性)暗藏在对象外部,不容许内部对象间接拜访对象的外部信息。然而能够提供一些能够被外界拜访的办法来操作属性。就如同咱们看不到挂在墙上的空调的外部的整机信息(也就是属性),然而能够通过遥控器(办法)来管制空调。如果属性不想被外界拜访,咱们大可不必提供办法给外界拜访。然而如果一个类没有提供给外界拜访的办法,那么这个类也没有什么意义了。就如同如果没有空调遥控器,那么咱们就无奈操控空凋制冷,空调自身就没有意义了(当然当初还有很多其余办法,这里只是为了举例子)。
public class Student {
private int id;//id 属性私有化
private String name;//name 属性私有化
// 获取 id 的办法
public int getId() {return id;}
// 设置 id 的办法
public void setId(int id) {this.id = id;}
// 获取 name 的办法
public String getName() {return name;}
// 设置 name 的办法
public void setName(String name) {this.name = name;}
}
继承
不同类型的对象,相互之间常常有肯定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的个性(班级、学号等)。同时,每一个对象还定义了额定的个性使得他们不同凡响。例如小明的数学比拟好,小红的性情惹人青睐;小李的力量比拟大。继承是应用已存在的类的定义作为根底建设新类的技术,新类的定义能够减少新的数据或新的性能,也能够用父类的性能,但不能选择性地继承父类。通过应用继承,能够疾速地创立新的类,能够进步代码的重用,程序的可维护性,节俭大量创立新类的工夫,进步咱们的开发效率。
对于继承如下 3 点请记住:
- 子类领有父类对象所有的属性和办法(包含公有属性和公有办法),然而父类中的公有属性和办法子类是无法访问,只是领有。
- 子类能够领有本人属性和办法,即子类能够对父类进行扩大。
- 子类能够用本人的形式实现父类的办法。(当前介绍)。
多态
多态,顾名思义,示意一个对象具备多种的状态,具体表现为父类的援用指向子类的实例。
多态的特点:
- 对象类型和援用类型之间具备继承(类)/ 实现(接口)的关系;
- 援用类型变量收回的办法调用的到底是哪个类中的办法,必须在程序运行期间能力确定;
- 多态不能调用“只在子类存在但在父类不存在”的办法;
- 如果子类重写了父类的办法,真正执行的是子类笼罩的办法,如果子类没有笼罩父类的办法,执行的是父类的办法。
接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都能够蕴含形象办法。
- 都能够有默认实现的办法(Java 8 能够用
default
关键字在接口中定义默认办法)。
区别:
- 接口次要用于对类的行为进行束缚,你实现了某个接口就具备了对应的行为。抽象类次要用于代码复用,强调的是所属关系(比如说咱们形象了一个发送短信的抽象类,)。
- 一个类只能继承一个类,然而能够实现多个接口。
- 接口中的成员变量只能是
public static final
类型的,不能被批改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被从新定义,也可被从新赋值。
深拷贝和浅拷贝区别理解吗?什么是援用拷贝?
对于深拷贝和浅拷贝区别,我这里先给论断:
- 浅拷贝:浅拷贝会在堆上创立一个新的对象(区别于援用拷贝的一点),不过,如果原对象外部的属性是援用类型的话,浅拷贝会间接复制外部对象的援用地址,也就是说拷贝对象和原对象共用同一个外部对象。
- 深拷贝:深拷贝会齐全复制整个对象,包含这个对象所蕴含的外部对象。
下面的论断没有齐全了解的话也没关系,咱们来看一个具体的案例!
浅拷贝
浅拷贝的示例代码如下,咱们这里实现了 Cloneable
接口,并重写了 clone()
办法。
clone()
办法的实现很简略,间接调用的是父类 Object
的 clone()
办法。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter 办法
@Override
public Address clone() {
try {return (Address) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter 办法
@Override
public Person clone() {
try {Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {throw new AssertionError();
}
}
}
测试:
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输入构造就能够看出,person1
的克隆对象和 person1
应用的依然是同一个 Address
对象。
深拷贝
这里咱们简略对 Person
类的 clone()
办法进行批改,连带着要把 Person
对象外部的 Address
对象一起复制。
@Override
public Person clone() {
try {Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {throw new AssertionError();
}
}
测试:
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输入构造就能够看出,尽管 person1
的克隆对象和 person1
蕴含的 Address
对象曾经是不同的了。
那什么是援用拷贝呢? 简略来说,援用拷贝就是两个不同的援用指向同一个对象。
我专门画了一张图来形容浅拷贝、深拷贝、援用拷贝:
Java 常见类
Object
Object 类的常见办法有哪些?
Object 类是一个非凡的类,是所有类的父类。它次要提供了以下 11 个办法:
/**
* native 办法,用于返回以后运行时对象的 Class 对象,应用了 final 关键字润饰,故不容许子类重写。*/
public final native Class<?> getClass()
/**
* native 办法,用于返回对象的哈希码,次要应用在哈希表中,比方 JDK 中的 HashMap。*/
public native int hashCode()
/**
* 用于比拟 2 个对象的内存地址是否相等,String 类对该办法进行了重写以用于比拟字符串的值是否相等。*/
public boolean equals(Object obj)
/**
* naitive 办法,用于创立并返回以后对象的一份拷贝。*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。倡议 Object 所有的子类都重写这个办法。*/
public String toString()
/**
* native 办法,并且不能重写。唤醒一个在此对象监视器上期待的线程(监视器相当于就是锁的概念)。如果有多个线程在期待只会任意唤醒一个。*/
public final native void notify()
/**
* native 办法,并且不能重写。跟 notify 一样,惟一的区别就是会唤醒在此对象监视器上期待的所有线程,而不是一个线程。*/
public final native void notifyAll()
/**
* native 办法,并且不能重写。暂停线程的执行。留神:sleep 办法没有开释锁,而 wait 办法开释了锁,timeout 是等待时间。*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数示意额定工夫(以毫微秒为单位,范畴是 0-999999)。所以超时的工夫还须要加上 nanos 毫秒。。*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的 2 个 wait 办法一样,只不过该办法始终期待,没有超时工夫这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable {}
== 和 equals() 的区别
==
对于根本类型和援用类型的作用成果是不同的:
- 对于根本数据类型来说,
==
比拟的是值。 - 对于援用数据类型来说,
==
比拟的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不论是比拟根本数据类型,还是援用数据类型的变量,其本质比拟的都是值,只是援用类型变量存的值是对象的地址。
equals()
不能用于判断根本数据类型的变量,只能用来判断两个对象是否相等。equals()
办法存在于 Object
类中,而 Object
类是所有类的间接或间接父类,因而所有的类都有 equals()
办法。
Object
类 equals()
办法:
public boolean equals(Object obj) {return (this == obj);
}
equals()
办法存在两种应用状况:
- 类没有重写
equals()
办法 :通过equals()
比拟该类的两个对象时,等价于通过“==”比拟这两个对象,应用的默认是Object
类equals()
办法。 - 类重写了
equals()
办法 :个别咱们都重写equals()
办法来比拟两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
举个例子(这里只是为了举例。实际上,你依照上面这种写法的话,像 IDEA 这种比拟智能的 IDE 都会提醒你将 ==
换成 equals()
):
String a = new String("ab"); // a 为一个援用
String b = new String("ab"); // b 为另一个援用, 对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
String
中的 equals
办法是被重写过的,因为 Object
的 equals
办法是比拟的对象的内存地址,而 String
的 equals
办法比拟的是对象的值。
当创立 String
类型的对象时,虚构机会在常量池中查找有没有曾经存在的值和要创立的值雷同的对象,如果有就把它赋给以后援用。如果没有就在常量池中从新创立一个 String
对象。
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;
}
hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引地位。
hashCode()
定义在 JDK 的 Object
类中,这就意味着 Java 中的任何类都蕴含有 hashCode()
函数。另外须要留神的是:Object
的 hashCode()
办法是本地办法,也就是用 C 语言或 C++ 实现的,该办法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对 (key-value),它的特点是: 能依据“键”疾速的检索出对应的“值”。这其中就利用到了散列码!(能够疾速找到所须要的对象)
为什么要有 hashCode?
咱们以“HashSet
如何查看反复”为例子来阐明为什么要有 hashCode
?
上面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象退出
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象退出的地位,同时也会与其余曾经退出的对象的hashCode
值作比拟,如果没有相符的hashCode
,HashSet
会假如对象没有反复呈现。然而如果发现有雷同hashCode
值的对象,这时会调用equals()
办法来查看hashCode
相等的对象是否真的雷同。如果两者雷同,HashSet
就不会让其退出操作胜利。如果不同的话,就会从新散列到其余地位。这样咱们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实,hashCode()
和 equals()
都是用于比拟两个对象是否相等。
那为什么 JDK 还要同时提供这两个办法呢?
这是因为在一些容器(比方 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考增加元素进 HashSet
的过程)!
咱们在后面也提到了增加元素进 HashSet
的过程,如果 HashSet
在比照的时候,同样的 hashCode
有多个对象,它会持续应用 equals()
来判断是否真的雷同。也就是说 hashCode
帮忙咱们大大放大了查找老本。
那为什么不只提供 hashCode()
办法呢?
这是因为两个对象的hashCode
值相等并不代表两个对象就相等。
那为什么两个对象有雷同的 hashCode
值,它们也不肯定是相等的?
因为 hashCode()
所应用的哈希算法兴许刚好会让多个对象传回雷同的哈希值。越蹩脚的哈希算法越容易碰撞,但这也与数据值域散布的个性无关(所谓哈希碰撞也就是指的是不同的对象失去雷同的 hashCode
)。
总结下来就是:
- 如果两个对象的
hashCode
值相等,那这两个对象不肯定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
办法也返回true
,咱们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,咱们就能够间接认为这两个对象不相等。
置信大家看了我后面对 hashCode()
和 equals()
的介绍之后,上面这个问题曾经难不倒你们了。
为什么重写 equals() 时必须重写 hashCode() 办法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
办法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
办法的话就可能会导致 equals
办法判断是相等的两个对象,hashCode
值却不相等。
思考:重写 equals()
时没有重写 hashCode()
办法的话,应用 HashMap
可能会呈现什么问题。
总结:
equals
办法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有雷同的
hashCode
值,他们也不肯定是相等的(哈希碰撞)。
更多对于 hashCode()
和 equals()
的内容能够查看:Java hashCode() 和 equals()的若干问题解答
String
String、StringBuffer、StringBuilder 的区别?
可变性
String
是不可变的(前面会详细分析起因)。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是应用字符数组保留字符串,不过没有应用 final
和 private
关键字润饰,最要害的是这个 AbstractStringBuilder
类还提供了很多批改字符串的办法比方 append
办法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {char[] value;
public AbstractStringBuilder append(String str) {if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
线程安全性
String
中的对象是不可变的,也就能够了解为常量,线程平安。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共办法。StringBuffer
对办法加了同步锁或者对调用的办法加了同步锁,所以是线程平安的。StringBuilder
并没有对办法进行加同步锁,所以是非线程平安的。
性能
每次对 String
类型进行扭转的时候,都会生成一个新的 String
对象,而后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象自身进行操作,而不是生成新的对象并扭转对象援用。雷同状况下应用 StringBuilder
相比应用 StringBuffer
仅能取得 10%~15% 左右的性能晋升,但却要冒多线程不平安的危险。
对于三者应用的总结:
- 操作大量的数据: 实用
String
- 单线程操作字符串缓冲区下操作大量数据: 实用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 实用
StringBuffer
String 为什么是不可变的?
String
类中应用 final
关键字润饰字符数组来保留字符串,所以String
对象是不可变的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {private final char value[];
//...
}
🐛 修改:咱们晓得被
final
关键字润饰的类不能被继承,润饰的办法不能被重写,润饰的变量是根本数据类型则值不能扭转,润饰的变量是援用类型则不能再指向其余对象。因而,final
关键字润饰的数组保留字符串并不是String
不可变的根本原因,因为这个数组保留的字符串是可变的(final
润饰援用类型变量的状况)。
String
真正不可变有上面几点起因:
- 保留字符串的数组被
final
润饰且为公有的,并且String
类没有提供 / 裸露批改这个字符串的办法。String
类被final
润饰导致其不能被继承,进而防止了子类毁坏String
不可变。相干浏览:如何了解 String 类型值的不可变?– 知乎发问
补充(来自 issue 675):在 Java 9 之后,
String
、StringBuilder
与StringBuffer
的实现改用byte
数组存储字符串。public final class String implements java.io.Serializable,Comparable<String>, CharSequence { // @Stable 注解示意变量最多被批改一次,称为“稳固的”。@Stable private final byte[] value;} abstract class AbstractStringBuilder implements Appendable, CharSequence {byte[] value; }
Java 9 为何要将
String
的底层实现由char[]
改成了byte[]
?新版的 String 其实反对两个编码方案:Latin-1 和 UTF-16。如果字符串中蕴含的汉字没有超过 Latin-1 可示意范畴内的字符,那就会应用 Latin-1 作为编码方案。Latin-1 编码方案下,
byte
占一个字节(8 位),char
占用 2 个字节(16),byte
相较char
节俭一半的内存空间。JDK 官网就说了绝大部分字符串对象只蕴含 Latin-1 可示意的字符。
如果字符串中蕴含的汉字超过 Latin-1 可示意范畴内的字符,
byte
和char
所占用的空间是一样的。这是官网的介绍:https://openjdk.java.net/jeps…。
字符串拼接用“+”还是 StringBuilder?
Java 语言自身并不反对运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
下面的代码对应的字节码如下:
能够看出,字符串对象通过“+”的字符串拼接形式,实际上是通过 StringBuilder
调用 append()
办法实现的,拼接实现之后调用 toString()
失去一个 String
对象。
不过,在循环内应用“+”进行字符串的拼接的话,存在比拟显著的缺点:编译器不会创立单个 StringBuilder
以复用,会导致创立过多的 StringBuilder
对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {s += arr[i];
}
System.out.println(s);
StringBuilder
对象是在循环外部被创立的,这意味着每循环一次就会创立一个 StringBuilder
对象。
如果间接应用 StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {s.append(value);
}
System.out.println(s);
如果你应用 IDEA 的话,IDEA 自带的代码查看机制也会提醒你批改代码。
String#equals() 和 Object#equals() 有何区别?
String
中的 equals
办法是被重写过的,比拟的是 String 字符串的值是否相等。Object
的 equals
办法是比拟的对象的内存地址。
字符串常量池的作用理解吗?
字符串常量池 是 JVM 为了晋升性能和缩小内存耗费针对字符串(String 类)专门开拓的一块区域,次要目标是为了防止字符串的反复创立。
// 在堆中创立字符串对象”ab“// 将字符串对象”ab“的援用保留在字符串常量池中
String aa = "ab";
// 间接返回字符串常量池中字符串对象”ab“的援用
String bb = "ab";
System.out.println(aa==bb);// true
更多对于字符串常量池的介绍能够看一下 Java 内存区域详解 这篇文章。
String s1 = new String(“abc”); 这句话创立了几个字符串对象?
会创立 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的援用,那么会在堆中创立 2 个字符串对象“abc”。
示例代码(JDK 1.8):
String s1 = new String("abc");
对应的字节码:
ldc
命令用于判断字符串常量池中是否保留了对应的字符串对象的援用,如果保留了的话间接返回,如果没有保留的话,会在堆中创立对应的字符串对象并将该字符串对象的援用保留到字符串常量池中。
2、如果字符串常量池中已存在字符串对象“abc”的援用,则只会在堆中创立 1 个字符串对象“abc”。
示例代码(JDK 1.8):
// 字符串常量池中已存在字符串对象“abc”的援用
String s1 = "abc";
// 上面这段代码只会在堆中创立 1 个字符串对象“abc”String s2 = new String("abc");
对应的字节码:
这里就不对下面的字节码进行具体正文了,7 这个地位的 ldc
命令不会在堆中创立新的字符串对象“abc”,这是因为 0 这个地位曾经执行了一次 ldc
命令,曾经在堆中创立过一次字符串对象“abc”了。7 这个地位执行 ldc
命令会间接返回字符串常量池中字符串对象“abc”对应的援用。
intern 办法有什么作用?
String.intern()
是一个 native(本地)办法,其作用是将指定的字符串对象的援用保留在字符串常量池中,能够简略分为两种状况:
- 如果字符串常量池中保留了对应的字符串对象的援用,就间接返回该援用。
- 如果字符串常量池中没有保留了对应的字符串对象的援用,那就在常量池中创立一个指向该字符串对象的援用并返回。
示例代码(JDK 1.8):
// 在堆中创立字符串对象”Java“// 将字符串对象”Java“的援用保留在字符串常量池中
String s1 = "Java";
// 间接返回字符串常量池中字符串对象”Java“对应的援用
String s2 = s1.intern();
// 会在堆中在独自创立一个字符串对象
String s3 = new String("Java");
// 间接返回字符串常量池中字符串对象”Java“对应的援用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
String 类型的变量和常量做“+”运算时产生了什么?
先来看字符串不加 final
关键字拼接的状况(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
留神:比拟 String 字符串的值是否相等,能够应用
equals()
办法。String
中的equals
办法是被重写过的。Object
的equals
办法是比拟的对象的内存地址,而String
的equals
办法比拟的是字符串的值是否相等。如果你应用==
比拟两个字符串是否相等的话,IDEA 还是提醒你应用equals()
办法替换。
对于编译期能够确定值的字符串,也就是常量字符串,jvm 会将其存入字符串常量池。并且,字符串常量拼接失去的字符串常量在编译阶段就曾经被寄存字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深刻了解 Java 虚拟机》中是也有介绍到:
常量折叠会把常量表达式的值求进去作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化简直都在即时编译器中进行)。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就能够确定值的常量才能够:
- 根本数据类型 (
byte
、boolean
、short
、char
、int
、float
、long
、double
) 以及字符串常量。 final
润饰的根本数据类型和字符串变量- 字符串通过“+”拼接失去的字符串、根本数据类型之间算数运算(加减乘除)、根本数据类型的位运算(<<、\>>、\>>>)
援用的值在程序编译期是无奈确定的,编译器无奈对其进行优化。
对象援用和“+”的字符串拼接形式,实际上是通过 StringBuilder
调用 append()
办法实现的,拼接实现之后调用 toString()
失去一个 String
对象。
String str4 = new StringBuilder().append(str1).append(str2).toString();
咱们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会从新创建对象。如果须要扭转字符串的话,能够应用 StringBuilder
或者 StringBuffer
。
不过,字符串应用 final
关键字申明之后,能够让编译器当做常量来解决。
示例代码:
final String str1 = "str";
final String str2 = "ing";
// 上面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final
关键字批改之后的 String
会被编译器当做常量来解决,编译器在程序编译期就能够确定它的值,其成果就相当于拜访常量。
如果,编译器在运行时能力晓得其确切值的话,就无奈对其优化。
示例代码(str2
在运行时能力确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创立的新的对象
System.out.println(c == d);// false
public static String getStr() {return "ing";}
参考
- 深刻解析 String#internhttps://tech.meituan.com/2014…
- R 大(RednaxelaFX)对于常量折叠的答复:https://www.zhihu.com/questio…
后记
近期文章精选:
- 往年找工作有点难!
- 上岸美团、华为、字节!
- 顺利找到工作了
- 八股文又又又更新了!
走近作者:
- 害,毕业三年了!
- 我的网站又降级啦!
- 1049 天,100K! 简略复盘!
如果本文对你有帮忙的话,欢送点赞 & 在看 & 分享,这对我持续分享 & 创作优质文章十分重要。感激🙏🏻