java 八股基础

79 阅读27分钟

JAVA基础

1.什么是JDK、JRE、JVM?

  1. JDK(Java Development Kit) :这是Java语言的软件开发工具包,主要用于移动设备、嵌入式设备上的Java应用程序。JDK是整个Java开发的核心,它包含了Java的运行环境(JVM+Java系统类库)和Java工具。具体来说,JDK中包含了编译器(如javac.exe)、打包工具(如jar.exe)等开发工具,还附带有JRE。
  2. JRE(Java Runtime Environment) :这是Java的运行环境,包含了Java虚拟机和Java程序运行时需要的一些核心类库。如果我们只需要运行Java程序或Applet,那么只需要安装JRE就可以了。值得注意的是,JDK中包含了JRE,所以如果安装了JDK就不需要单独再安装JRE了。
  3. JVM(Java Virtual Machine) :这是Java虚拟机,是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

综上所述,JDK、JRE和JVM都是Java程序开发或运行过程中必不可少的工具。

2.什么是字节码,使用字节码的好处是什么?

在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。

使用字节码的好处包括:

  1. 跨平台:Java字节码是平台无关的,可以在任何支持Java虚拟机的平台上运行。
  2. 安全性高:由于Java字节码是由编译器生成的,因此可以对其进行验证以确保其安全性。
  3. 性能优化:Java虚拟机可以根据硬件环境以及运行时情况等因素进行动态优化,提高程序的性能。
  4. 加密保护:Java字节码可以通过加密保护,从而防止被恶意用户反编译。

综上所述,使用字节码的好处非常明显,特别是在需要实现跨平台、安全性高以及优化性能等方面,Java字节码发挥了巨大的作用。

3.为什么说JAVA编译与解释共存

JAVA语言被称为“编译与解释并存”的原因是它在执行过程中同时使用了编译和解释两种方式。

首先,在JAVA开发过程中,源代码会经过编译器(如javac)将其转换成字节码文件(.class),这个过程就是编译。编译后的字节码文件可以在任何支持Java虚拟机(JVM)的平台上运行。

其次,当Java程序运行时,Java虚拟机会对字节码进行解释或即时编译为机器码再执行。解释执行是逐条解释执行字节码指令,而即时编译则是将热点代码(经常执行的代码)编译成本地机器码,以提高执行效率。

为了实现Java跨平台的特性,JAVA避免使用编译将高级语言直接转换为机器语言,因此产生了解释器逐行解释字节码的想法。但是单单由解释器承担翻译的工作,将字节码文件解释成高级语言效率太低。所以导致了编译和解释并存的特点。

4.成员变量与局部变量的区别?

Java中的成员变量和局部变量在许多方面都是不同的。以下是一些主要的区别:

  1. 作用域:成员变量也被称为实例变量,它们属于类的实例。它们在整个类的范围内都是可见的,也就是说,在类中的任何方法(包括静态和非静态的)都可以访问它们。局部变量是在方法内部定义的变量,它们的范围仅限于它们被定义的方法。一旦出了这个方法,它们就不再存在。
  2. 生命周期:成员变量从对象创建时开始存在,直到对象被销毁时才结束。局部变量只存在于它们被声明的那个方法执行期间。当方法执行结束后,局部变量就会消失。
  3. 初始值:成员变量如果没有显式地初始化,那么它们的默认值会根据它们的类型决定。对于数值类型(如int, float等),默认值是0;对于引用类型(如String, Array等),默认值是null。局部变量在声明时必须初始化,否则编译器会报错。
  4. 存储位置:成员变量存储在堆内存中,而局部变量存储在栈内存中。
  5. 访问修饰符:成员变量可以由public, private, protected和default(如果没有指定访问修饰符,那么默认就是default)访问修饰符修饰,而局部变量不能被访问修饰符修饰。但是,成员变量和局部变量都能被 final 所修饰。

5静态变量有什么作用?

首先,静态变量可以在多个对象之间共享相同的值。当一个对象对静态变量进行修改时,其他对象可以立即访问到这个修改后的值。

其次,静态变量可以节省内存。由于静态变量属于类而不是实例,所以只会在内存中存在一份副本。

此外,静态变量方便访问。静态变量可以通过类名直接访问,无需创建对象。这使得在没有实例化对象的情况下也能够使用该变量。

但是,使用静态变量时需要注意线程安全性问题。由于静态变量是共享的,多个线程同时修改静态变量可能导致数据不一致或竞态条件。需要额外的同步机制来保证线程安全。

另外,子类不能重写父类的静态变量,而是会隐藏父类的静态变量。这可能导致在使用继承时出现意料之外的结果。

因此,在使用静态变量时需要根据具体情况权衡利弊。

6.字符型常量和字符串常量的区别?

在Java中,字符型常量(char)和字符串常量(String)虽然都是常量,但它们之间存在一些重要的区别。

  1. 数据类型:字符型常量是单个字符,其数据类型为char。而字符串常量是由字符组成的字符串,其数据类型为String。
  2. 表示方式:字符型常量使用单引号(')来表示,例如'A'。而字符串常量使用双引号(")来表示,例如"Hello, World!"。
  3. 存储和内存:字符型常量在内存中以Unicode编码的形式存储,占用的内存空间为2字节(16位)。而字符串常量在内存中以字符数组的形式存储,占用的内存空间取决于字符串的长度。
  4. 操作和方法:字符型常量和字符串常量都可以进行一些基本的操作,如比较、连接等。但字符串常量具有更多专门用于字符串操作的方法,如indexOf(), substring(), replace()等。

7.静态方法和实例方法有何不同?

静态方法和实例方法的主要区别体现在以下几个方面:

  1. 调用方式:静态方法属于整个类所有,因此调用它不需要实例化,可以直接通过类名调用。实例方法必须先实例化,创建一个对象,才能进行调用。
  2. 可访问的成员:静态方法只能访问静态成员,不能访问实例成员;而实例方法可以访问静态成员和实例成员。
  3. 内存占用和回收:静态方法在程序运行期间是一直存放在内存中,因此调用速度快,但是却占用内存。实例方法是使用完成后由回收机制自动进行回收,下次再使用必须再实例化。
  4. 变量存储:静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
  5. 加载时机:静态方法和静态变量随着类的加载而加载,所以静态方法不能使用实例变量,只能使用静态变量。

8.重载和重写有什么区别?

重载: 就是同样的一个方法能够根据输入数据的不同,做出不同的处理. 重写: 就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法重载它们.

在以下方面存在一些差异:

以下是一个关于重载(Overloading)和重写(Overriding)的对比表格:

重载(Overloading)重写(Overriding)
方法名必须相同必须相同
参数列表不同(类型、数量、顺序至少有一项不同)完全相同(类型、数量、顺序都相同)
返回类型可以不同相同或为其子类
异常类型可以不同比被重写方法声明的异常更少、更有限,或无异常
访问修饰符可以不同(public、protected、private)相同或更广泛的访问修饰符
调用方式通过传递不同的参数来调用通过对象引用调用,参数列表完全匹配被重写的方法的参数列表
目的提供多个方法,以方便使用不同参数调用同一个功能子类实现父类中的特定方法,以满足子类的需求
示例代码public void print(int i) { ... }public void print(String s) { ... }public class Animal { public void move(); }public class Dog extends Animal { public void move(); }

9.JAVA基本数据类型

Java有八种基本数据类型:

  1. byte(字节型):占用1个字节,取值范围是-128到127。
  2. short(短整型):占用2个字节,取值范围是-32768到32767。
  3. int(整型):占用4个字节,取值范围是-2147483648到2147483647。
  4. long(长整型):占用8个字节,取值范围是-9223372036854775808到9223372036854775807。
  5. float(单精度浮点型):占用4个字节,可以提供7位有效数字。
  6. double(双精度浮点型):占用8个字节,可以提供15位有效数字。
  7. char(字符型):占用2个字节,可以表示一个16位的Unicode字符。
  8. boolean(布尔型):占用1个字节,只有两个取值:true和false。

10.基本类型与包装类型的区别

基本类型和包装类型在Java中是不同的数据类型。以下是它们的主要区别:

  1. 存储位置:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰)存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  2. 初始值:成员变量包装类型不赋值就是 null,而基本类型有默认值且不是 null;
  3. 泛型适用性:包装类型可用于泛型,但基本类型不可以。
  4. 包含内容和性质:相比于对象类型,基本数据类型占用的空间非常小。基本数据类型只包含数据,而包装器类则包含数据和操作;

11.自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来; 拆箱:将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的 Valueof() 方法, 拆箱其实就是调用了xxxValue() 方法。 例子:

//下面2个代码等价
Integer i = 10 
Integer i = Integer.valueOf(10)
// 下面2个代码等价 
int n =i
int n = i.intValue();

12.包装类型的缓存机制

。在Java中,包装类型指的是将基本类型(如int,char,double等)转换为对应的包装类(如Integer,Character,Double等)。Java自动装箱和自动拆箱特性可以让我们在使用包装类型时,无需显式地调用包装类的构造函数或方法。

Byte, Short, Integer,Long 这4 种包装类默认创建了数值**[-128,127]的相应类型的缓存数据,Character 创建了数值在[0,127]**范围的缓存数据,Boolean 直接返回 True or False

即如果创建的对象的值在这个范围内,那么会直接从缓存中取对象,而不是重新创建新的对象。这个特性在处理小整数的频繁创建和销毁时,可以显著提高性能。

缓存机制是通过静态变量实现的。Integer类中有一个静态变量缓存,存储了-128到127之间的所有Integer对象。当创建一个Integer对象且其值在这个范围内时,Java会直接返回缓存中的对象。

在使用==运算符比较Integer对象时,需要考虑创建这些对象的上下文。如果对象是在缓存范围内创建的,那么使用==运算符是有效的。如果对象是在缓存范围之外创建的,则可能需要使用equals()方法来比较它们的值。

Integer a = 8;
Integer b = 8;
System. out.println(a==b);          // true
a = 128b = 128;
System. out.println(a ==b);         // false
System. out.println(a.equals(b)); // true

13.浮点运算丢失精度的原因

Java浮点运算中会发生精度丢失,这主要是由于计算机对浮点数的存储方式和浮点数的运算规则导致的。

  1. 存储精度限制:在计算机中,浮点数是以二进制的形式存储的。然而,有些数用二进制无法精确表示,例如0.1。在浮点数的存储中,由于精度限制,不能保证所有的浮点数都能精确表示。因此,在计算时可能会产生微小的误差。
  2. 运算规则:浮点数的运算规则也可能会导致精度丢失。例如,在进行加法和乘法运算时,可能会产生溢出、下溢或舍入误差。此外,在进行除法运算时,如果除数不能被精确表示,也会导致误差。

为了减少浮点运算中的精度丢失,可以采取以下措施:

  1. 使用BigDecimal类进行高精度计算。
  2. 对输入数据进行校验,确保输入的数据符合预期的范围和精度。
  3. 在进行计算时,尽量避免大数与小数之间的运算,以减少误差的积累。
  4. 对输出结果进行适当的四舍五入或截断处理,以减少精度丢失对结果的影响。

14.超过long整形的数据应该如何表示

在Java中,如果你需要表示的整数超过了long的最大值(9223372036854775807),你可以使用BigInteger类。BigInteger是一个可以表示任意大小的整数的类,它没有长度的限制。

下面是一个使用BigInteger的例子:

import java.math.BigInteger;  
  
public class Main {  
    public static void main(String[] args) {  
        BigInteger bigInteger = new BigInteger("9223372036854775808"); // 这个数超过了long的最大值  
        System.out.println(bigInteger);  
    }  
}

但是需要注意,虽然BigInteger可以表示很大的整数,但是它不支持直接的数学运算(例如加法、乘法等),你需要使用BigInteger类中提供的方法来进行计算。这可能会比使用intlong等类型进行计算要慢,因此在使用BigInteger时需要考虑性能问题。

15.构造方法有哪些特点?是否可被 override?

构造方法有以下特点:

  1. 构造方法的方法名必须与类名相同。
  2. 构造方法没有返回类型,也不能定义为void,在方法名前面不声明方法类型。
  3. 构造方法的主要作用是完成对象的初始化工作,它能够把定义对象时的参数传给对象的域。
  4. 构造方法不由编程人员调用,而要系统调用。
  5. 一个类可以定义多个构造方法,如果在定义类时没有定义构造方法,则编译系统会自动插入一个无参数的默认构造器,这个构造器不执行任何代码。
  6. 构造方法可以重载,以参数的个数、类型或排列顺序区分。

至于构造方法是否可以被override,答案是不可以。构造方法不能被继承,因此也不能被重写(override)。

16.面向对象的三大特征

面向对象的三大特征包括封装、继承和多态。

  1. 封装:将对象的属性和行为封装起来,不需要让外界知道具体实现细节,这就是封装思想。
  2. 继承:一个类(叫做子类)继承另一个类(叫做基类)的功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。继承是类与类之间最常见的一种关系,它是一种典型的“is-a”关系。
  3. 多态:指允许不同类的对象对同一消息作出响应,但都要用自己的代码来执行。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。

以上就是面向对象的三大特征,这些特征为面向对象编程提供了基础和保障。

17.接口与抽象类有什么区别

共同点:

  1. 都不能被实例化。
  2. 都可以包含抽象方法。
  3. 都可以有默认实现的方法(Java 8可以用 default 关键字在接口中定义默 认方法)。

区别:

  1. 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的 行为。抽象类主要用于代码复用,强调的是所属关系。
  2. 一个类只能继承一个类,但是可以实现多个接口。
  3. 接口中的成员变量只能是 public static final 类型的,不能被修改且必须 有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

18.深拷贝与浅拷贝的区别

在Java中,深拷贝和浅拷贝是两种复制对象的方法,它们在复制对象时处理对象内部引用的方式不同。

浅拷贝(Shallow Copy)

浅拷贝是针对对象的引用进行复制,也就是说,如果一个对象包含对另一个对象的引用,那么进行浅拷贝时,这个引用会被复制,而不是被共享。换句话说,如果你改变原始对象中的这个引用指向的对象,那么在浅拷贝出来的对象中,这个引用指向的对象也会发生改变。

例如,如果你有一个包含列表的列表(即二维数组),并对其进行浅拷贝,那么拷贝出来的列表实际上是指向原始列表中元素的引用。所以,如果你更改原始列表中的元素,那么在浅拷贝出来的列表中也会看到这些更改。

深拷贝(Deep Copy)

深拷贝是相对于浅拷贝而言的。在进行深拷贝时,如果一个对象包含对另一个对象的引用,那么这个引用会被复制,而不是被共享。但是,对于被引用的对象,深拷贝会递归地进行复制,也就是说,被引用的对象中的所有元素都会被复制,而不仅仅是引用。

例如,如果你有一个包含列表的列表(即二维数组),并对其进行深拷贝,那么拷贝出来的列表中的元素是原始列表中相应元素的副本。所以,如果你更改原始列表中的元素,那么在深拷贝出来的列表中不会看到这些更改。

总的来说,浅拷贝和深拷贝的主要区别在于如何处理对象内部的引用。浅拷贝只复制引用,而深拷贝则递归地复制被引用的对象中的所有元素。

19.Object类的常用方法

在Java中,Object类是所有类的基类,它定义了一些常用的方法。以下是一些常用的Object类方法:

  1. public final Class<?> getClass(): 返回当前对象的运行时类。
  2. public int hashCode(): 返回对象的哈希码值,通常与对象的内存地址有关。
  3. public boolean equals(Object obj): 比较此对象与指定对象的等价性。
  4. protected Object clone() throws CloneNotSupportedException: 创建并返回此对象的一个副本。
  5. public String toString(): 返回对象的字符串表示。
  6. protected void finalize() throws Throwable: 当垃圾收集器确定不存在对该对象的更多引用时,由对象的垃圾收集器调用此方法。
  7. public final void notify(): 唤醒正在等待对象监视器的单个线程。
  8. public final void notifyAll(): 唤醒正在等待对象监视器的所有线程。
  9. public final void wait(long timeout) throws InterruptedException: 使当前线程等待直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法,或经过指定的时间量。
  10. public final void wait(long timeout, int nanos) throws InterruptedException: 使当前线程等待直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法,或经过指定的时间量和纳秒数。

以上是Object类的一些常用方法,需要注意的是,这些方法中的一部分在派生类中可能被覆盖或重写。

20.==和 equals的区别

在Java中,==和.equals()是用于比较两个对象是否相等的方法,但它们的比较方式是不同的。

==操作符在Java中主要用于比较两个变量的值是否相等。如果两个变量的值相等,那么==操作符就会返回true。然而,这个操作符只能用于比较基本数据类型(如int,char,boolean等),不能用于比较对象。对于对象,==操作符比较的是两个对象的引用是否相等,即它们是否指向内存中的同一个对象。

.equals()方法则用于比较两个对象的内容是否相等。它是Object类中的一个方法,所有类都继承自Object类,因此所有类都有这个方法。默认情况下,所有类继承的.equals()方法的行为和==操作符一样,也是比较引用是否相等。但如果你重写了这个方法,那么它就会按照你的要求去比较对象的内容。比如,对于String类,.equals()方法比较的是字符串的内容是否相同,而不是引用是否相同。

总结一下,==操作符用于比较基本数据类型的值和对象的引用是否相等,.equals()方法用于比较对象的引用和内容是否相等(默认行为和==相同,但可以通过重写改变)。

21.hashCode()的作用

hashCode() 是Java中Object类的一个方法,它用于返回对象的哈希码。哈希码是一个整数,它是由对象的内存地址、字段值等属性计算出来的。

hashCode() 的主要用途包括:

  1. 用于散列表(Hash Table): 哈希表是一种数据结构,它使用哈希函数将键映射到桶中,然后存储相应的值。在Java中,例如,HashMap、HashSet等数据结构就使用了哈希表。在这些数据结构中,hashCode() 方法被用来确定对象应该被放置在哪个桶中。
  2. 加快查找速度: 在散列表中,通过哈希码可以在O(1)时间复杂度内找到相应的对象,而不需要遍历整个集合。这是因为哈希表将数据分布到不同的桶中,通过桶的索引可以直接找到对应的数据,而不需要逐一比较。
  3. 判断两个对象是否相等: 在Java中,如果两个对象是equals的,那么它们的哈希码一定相等。因此,可以通过比较两个对象的哈希码来判断它们是否可能相等。然后,如果两个对象的哈希码相等,再通过 equals() 方法进行详细比较。这种策略可以提高效率,因为计算哈希码通常比执行复杂的比较操作要快得多。

需要注意的是,虽然如果两个对象的 hashCode() 相同,它们可能相等,但两个不同的对象也有可能具有相同的哈希码。因此,不应该仅仅依赖 hashCode() 来判断两个对象是否相等。同时,为了让 hashCode() 方法在散列表等数据结构中正常工作,一般需要确保重写 equals() 方法时也重写 hashCode() 方法。

22.为什么要有hashCode

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

23.为什么重写 equals() 时必须重写hashCode()方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

24.String、 String Buffer、 String Builder 的区别?

String、StringBuffer和StringBuilder是Java中用于处理字符串的类,它们有以下区别:

  1. 不可变性:String类是不可变的,一旦创建了字符串对象,就无法修改其内容。而StringBuffer和StringBuilder是可变的,可以修改字符串的内容。
  2. 线程安全:String类和StringBuffer类是线程安全的,而StringBuilder类不是线程安全的。
  3. 性能:在性能方面,StringBuilder类通常要优于StringBuffer类,因为StringBuilder类去掉了线程安全的部分,有效减小了开销。
  4. 适用场景:String类适用于少量的字符串操作的情况,StringBuffer类适用于多线程下在字符缓冲区进行大量操作的情况,而StringBuilder类适用于单线程下在字符缓冲区进行大量操作的情况。

综上所述,选择哪个类取决于具体的使用场景。如果需要不可变性,可以选择String类;如果需要在多线程环境下进行字符串操作,可以选择StringBuffer类;如果需要在单线程环境下进行字符串操作,且追求更高的性能,可以选择StringBuilder类。

25.String为什么不可变?

  1. 保存字符串的数组被 final 修饰且为私有的,并且 String 类没有提供暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

26.String#equals() 和 Object#equals0 有何区别?

String#equals()和 Object#equals())有何区别?String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的equals 方法是比较的对象的内存地址。

27.字符串常量池

字符串常量池是Java语言中的一种机制,它用于存储字符串常量对象。在Java中,字符串是不可变的,因此相同的字符串常量可以在不同的变量之间共享。

字符串常量池位于Java堆内存中,它存储了编译期间存在的所有字符串常量对象的引用,以及运行时动态添加的引用。当创建一个字符串常量时,Java虚拟机(JVM)会首先在字符串常量池中查找是否已存在相同内容的字符串常量对象。如果存在,则将新变量引用该对象;如果不存在,则在字符串常量池中新建一个字符串常量对象,并让新变量引用它。

这种机制可以避免重复创建相同的字符串常量对象,从而减少了内存占用,提高了性能。同时,由于字符串常量池中的字符串常量对象是共享的,因此对字符串的操作(如拼接、替换等)不会创建新的字符串常量对象,而是修改原有对象的内容,从而进一步减少了内存占用,提高了性能。

总之,字符串常量池是Java语言中一个重要的机制,它通过共享字符串常量对象来优化性能和减少内存占用。

28.String s1 = new String("abc");这句话创建了几个字符串对象?

会创建1或2个字符串对象。

  1. 如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建2个字符串对象“abc”。
  2. 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建1个字符串对象“abc”。

29.intern 方法有什么作用?

intern方法是Java的String类中的一个方法,其主要作用是将字符串添加到字符串常量池中。

如果字符串常量池中已经存在一个与intern方法所代表的字符串相等的字符串,那么intern方法会直接返回常量池中的字符串。如果字符串常量池中不存在相等的字符串,那么它会在字符串常量池中创建一个新的字符串对象,并返回这个新字符串对象的引用。

使用intern方法的主要优势在于,当程序需要大量使用相同的字符串时,如果每次都创建新的字符串对象,会导致内存占用过多,降低程序的性能。而通过使用intern方法,可以将相同内容的字符串对象放入常量池中,这样相同的字符串只需要在内存中存储一份,可以实现字符串的复用,减少内存占用,提高程序的性能。