面向对象特性

1.接口和抽象类有什么区别?

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。

包含抽象方法的类叫做抽象类

创建抽象类和抽象方法是有帮助的,这是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法

一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。

接口的作用域中的所有东西都是public static final的,因为:

  • public:因为接口的主要目的是定义公共的API,所以字段必须是公开的
  • static:字段属于接口本身,而不是实现该接口的类的实例
  • final:确保字段的值不能被修改,保持接口的不可变性

所以我们通常使用接口的字段作为常量定义,这也是为什么经常看到接口名以”Constants”结尾,如:

1
2
3
4
interface DatabaseConstants {
int MAX_CONNECTIONS = 100;
String DEFAULT_URL = "localhost";
}

Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法。接口的基本概念仍然没变,介于类型之上、实现之下。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。


原文链接:https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Java%e6%a0%b8%e5%bf%83%e6%8a%80%e6%9c%af%e9%9d%a2%e8%af%95%e7%b2%be%e8%ae%b2/13%20%e8%b0%88%e8%b0%88%e6%8e%a5%e5%8f%a3%e5%92%8c%e6%8a%bd%e8%b1%a1%e7%b1%bb%e6%9c%89%e4%bb%80%e4%b9%88%e5%8c%ba%e5%88%ab%ef%bc%9f.md

(1.概念和使用目的 2.如何使用 3.实践上如何区分)

接口和抽象类是Java面向对象设计的两个基础机制。

接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何field都是隐含着public static final的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java标准类库中,定义了非常多的接口,比如java.util.List。

抽象类是不能实例化的类,用abstract关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和一般的Java类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java标准库中,比如collection框架,很多通用部分就被抽取成为抽象类,例如java.util.AbstractList。

Java类实现interface使用implements关键词,继承abstract class则是使用extends关键词

2.方法重写和重载有什么区别?

原文链接:https://segmentfault.com/a/1190000040879281

  1. 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  2. 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  3. 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

3.为什么需要内部类?什么是匿名内部类?

内部类的核心存在价值在于解决Java多重继承的限制问题。具体来说:

  • 每个内部类都能独立地继承自一个接口或类,这使得外部类可以通过内部类来间接实现多个接口的功能

  • 无论外部类是否已经继承了某个类或实现了某个接口,对内部类都没有影响

  • 内部类提供了可以继承多个具体的或抽象的类的能力

  • 实现接口更加灵活。当一个类需要实现某个接口,但又不想把所有的实现逻辑都放在主类中时,可以使用内部类来实现该接口,使得代码结构更加清晰。这种设计让多重继承的解决方案变得更加完整和灵活


匿名内部类是一种没有名字的内部类,它在声明的同时就进行实例化。主要用于:

  • 实现接口或抽象类的一次性使用场景
  • 适合创建只需要使用一次的类

例如:

1
2
3
4
5
6
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击");
}
});

长得很像js里的调用函数(

4.静态内部类和非静态内部类有什么区别?

非静态内部类:

  • 持有外部类的引用,可以访问外部类的所有成员(包括私有成员)
  • 必须先创建外部类实例才能创建内部类实例
  • 内存占用较大,因为每个实例都持有外部类的引用

静态内部类:

  • 不持有外部类的引用,只能访问外部类的静态成员
  • 可以直接创建实例,不需要外部类实例
  • 内存占用更小,更加高效

5.静态内部类的使用场景是什么?

通俗来说,静态内部类就像是一个可以独立运作的模块,虽然它在代码上”住”在外部类里面,但它的运行不依赖外部类的实例,更像是一个”独立的房间”。

使用场景有:

当这个内部类不需要访问外部类的实例成员时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class School {
private String name;

// 好的使用方式:不需要访问外部类的实例成员
public static class Student {
private String id;
private String name;
}

// 不适合用静态内部类:需要访问外部类实例成员
public class Teacher {
public void printSchool() {
System.out.println(School.this.name);
}
}
}

当内部类有独立的功能时:

1
2
3
4
5
6
7
8
9
10
public class File {
private String content;

// 独立的文件路径处理功能
public static class Path {
public static String normalize(String path) {
return path.replace("\\", "/");
}
}
}

需要延迟加载某些功能时:

1
2
3
4
5
6
7
8
9
10
11
12
public class Bank {
private static String name = "XX银行";

// 使用静态内部类实现延迟加载单例
private static class BankHolder {
private static final Bank INSTANCE = new Bank();
}

public static Bank getInstance() {
return BankHolder.INSTANCE;
}
}

语言特性

1.形参和实参的区别是什么?

形参: 就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。

实参: 就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。

在Java方法调用的过程中,就是把实参传递给形参,形参的作用域在方法内部。

2.值传递和引用传递区别是什么?

值传递: 是指在调用方法时,将实际参数拷贝一份传递给方法,这样在方法中修改形式参数时,不会影响到实际参数。

引用传递: 也叫地址传递,是指在调用方法时,将实际参数的地址传递给方法 (如C++中的引用&) ,这样在方法中对形式参数的修改,将影响到实际参数。

也就是说值传递,传递的是副本。引用传递,传递的是实际内存地址。

3.Java是值传递还是引用传递?

参考链接:https://juejin.cn/post/7153986445496516645

值传递。

当传递基本数据类型,比如原始类型(int、long、char等)、包装类型(Integer、Long、String等),实参和形参都是存储在不同的栈帧内,修改形参的栈帧数据,不会影响实参的数据。

当传参的引用类型,形参和实参指向同一个地址的时候,修改形参地址的内容,会影响到实参。当形参和实参指向不同的地址的时候,修改形参地址的内容,并不会影响到实参。这里在参数传递的过程中,只是把实参的地址拷贝了一份传递给形参,只修改了参数地址里面的内容,并没有对形参本身进行修改。

4.final作用是什么?

我们可以将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不许修改的。

作用:

  • 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成 final。
  • final 变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。

5.final、finally、finalize 有什么不同?

参考链接:https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Java%20%e6%a0%b8%e5%bf%83%e6%8a%80%e6%9c%af%e9%9d%a2%e8%af%95%e7%b2%be%e8%ae%b2/03%20%20%e8%b0%88%e8%b0%88final%e3%80%81finally%e3%80%81%20finalize%e6%9c%89%e4%bb%80%e4%b9%88%e4%b8%8d%e5%90%8c%ef%bc%9f-%e6%9e%81%e5%ae%a2%e6%97%b6%e9%97%b4.md

final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)

finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。

finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated。

6.static作用是什么?

参考链接:https://zhuanlan.zhihu.com/p/70110497

  1、static是一个修饰符,用于修饰成员。(成员变量,成员函数)static修饰的成员变量 称之为静态变量或类变量。

  2、static修饰的成员被所有的对象共享。

  3、static优先于对象存在,因为static的成员随着类的加载就已经存在

  4、static修饰的成员多了一种调用方式,可以直接被类名/静态成员所调用

  5、static修饰的数据是共享数据,对象中的存储的是特有的数据。

7.static和final区别是什么?

  1. 目的不同
  • static: 用于共享(属于类)
  • final: 用于不可变(防止修改)
  1. 使用时机不同
  • static:

    • 需要在多个实例间共享数据
    • 不需要访问实例数据的工具方法
  • final:

    • 常量定义
    • 防止方法被重写
    • 防止类被继承
  1. 可以组合使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Constants {
    // 静态常量:既共享又不可变
    static final double PI = 3.14159;

    // 静态最终方法:既可以直接调用,又不能被重写
    static final void printVersion() {
    System.out.println("v1.0");
    }
    }

使用建议:

  1. 工具类的方法通常是 static 的
  2. 配置常量通常是 static final 的
  3. 涉及安全的方法考虑用 final
  4. 不要过度使用 static,可能会增加类之间的耦合

数据类型

1.Java基本数据类型有几种?各占多少?

参考链接:https://dunwu.github.io/javacore/pages/55d693

Java 语言提供了 8 种基本类型,大致分为 4

2.基础类型和包装类有什么区别?

参考链接:https://cloud.tencent.com/developer/article/1579086

(1)包装类型可以为 null,而基本类型不可以

别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Writer {
private Integer age;
private String name;

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

那为什么 POJO 的属性必须要用包装类型呢?原因在于,数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱(将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值),就会抛出 NullPointerException 的异常。

(2)包装类型可用于泛型,而基本类型不可以

1
2
List<int> list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType
List<Integer> list = new ArrayList<>();

为什么呢?因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。

(3) 基本类型比包装类型更高效

基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。假如没有基本类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。

3.自动装箱与拆箱了解吗?原理是什么?

引入包装类的目的就是:提供一种机制,使得基本数据类型可以与引用类型互相转换

基本数据类型与包装类的转换被称为装箱拆箱

Java SE5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。

1
2
Integer chenmo  = 10;  // 自动装箱
int wanger = chenmo; // 自动拆箱

上面这段代码使用 JAD 反编译后的结果如下所示:

1
2
Integer chenmo = Integer.valueOf(10);
int wanger = chenmo.intValue();

也就是说,自动装箱是通过 Integer.valueOf() 完成的;自动拆箱是通过 Integer.intValue() 完成的。

4.int和Integer区别是什么?

一个是基本数据类型,一个是包装类,是一个对象。

使用 Integer 的场景:

  • 需要使用对象方法的场景
  • 可能存在 null 值的情况
  • 作为集合类的元素(集合不支持基本类型)

Integer方法:

1
2
3
4
5
6
7
8
Integer num = 100;
// 转换为其他数据类型
String str = num.toString();
double d = num.doubleValue();

// 解析字符串
int parsed = Integer.parseInt("100");
Integer value = Integer.valueOf("100");

5.Integer缓存机制了解吗?

1
2
3
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info("\nInteger a = 127;\nInteger b = 127;\na == b ? {}", a == b); // true

当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。

一道面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1)基本类型和包装类型 True 自动拆箱
int a = 100;
Integer b = 100;
System.out.println(a == b);

// 2)两个包装类型 True 缓存机制
Integer c = 100;
Integer d = 100;
System.out.println(c == d);

// 3)False 缓存超限
c = 200;
d = 200;
System.out.println(c == d);

6.怎么避免浮点数精度丢失的问题?

参考链接:https://javaguide.cn/java/basis/bigdecimal.html

为什么浮点数 floatdouble 运算的时候会有精度丢失的风险呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

因此,浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。

想要解决浮点数运算精度丢失这个问题,可以直接使用 BigDecimal 来定义浮点数的值,然后再进行浮点数的运算操作即可。

7.如何比较两个BigDecimal是否相等?

如何创建BigDecimal而不丢失精度?我们在使用 BigDecimal 时,为了防止精度丢失,推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建对象。


《阿里巴巴 Java 开发手册》中提到:

compareTo() 方法可以比较两个 BigDecimal 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。

1
2
3
BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("1.0");
System.out.println(a.compareTo(b));//0

其他常见BigDecimal方法:

  • 加减乘除
1
2
3
4
5
6
7
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.add(b));// 1.9
System.out.println(a.subtract(b));// 0.1
System.out.println(a.multiply(b));// 0.90
System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11

这里需要注意的是,在我们使用 divide 方法的时候尽量使用 3 个参数版本,并且RoundingMode 不要选择 UNNECESSARY,否则很可能会遇到 ArithmeticException(无法除尽出现无限循环小数的时候),其中 scale 表示要保留几位小数,roundingMode 代表保留规则。

1
2
3
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
return divide(divisor, scale, roundingMode.oldMode);
}
  • 保留几位小数
1
2
3
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,RoundingMode.HALF_DOWN);
System.out.println(n);// 1.255

常见类

1.Object 类的常见方法有哪些?

参考链接:https://www.runoob.com/java/java-object-class.html

2.==和 equals()的区别是什么?

参考链接:https://mp.weixin.qq.com/s/frUEQZ18ZJVVaViSVhlfaA

现在,让我们总结一下 == 操作符和 equals() 方法的区别:

  • == 操作符比较对象的引用,判断是否为同一对象。
  • equals() 是 Object 类中定义的方法,用于比较对象的内容,但默认情况下与 == 的行为相同(比较引用)。
  • 一些类会覆盖 equals() 方法以便在内容上进行比较,例如 String、Integer、Double 等。

当你需要比较对象的内容时,务必使用适当的方法。同时要注意处理参数为 null 的情况,以及在自定义类中正确地覆盖 equals() 方法,以便在语义上正确地比较对象内容。

String类源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

User类重写equals:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Objects;


public class User {
private String name;
private String address;
private String userId;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(userId, user.userId);
}

@Override
public int hashCode() {
return Objects.hash(userId);
}

//......
}

3.为什么要有 hashCode?

主要原因:性能优化

在哈希表(如HashMap、HashSet)这类数据结构中,如果仅使用equals比较对象是否相等,需要逐个比较,时间复杂度为O(n);

而使用hashCode可以先计算对象的哈希值,快速定位到存储桶(bucket),将时间复杂度优化到接近O(1)。

只有在发生哈希碰撞时才需要使用equals进行详细比较

4.hashCode和equals区别是什么?

hashCode和equals方法的区别可以从以下几个方面来说明:

  1. 设计目的不同:
  • equals: 用于判断两个对象的内容是否相等
  • hashCode: 用于获取对象的哈希码,主要用于在散列数据结构中快速查找对象
  1. 关系约定:
  • 如果两个对象equals相等,它们的hashCode必须相等
  • 如果两个对象hashCode相等,它们的equals不一定相等(可能发生哈希碰撞)
  • 如果两个对象hashCode不相等,它们的equals一定不相等

hashCode计算相对简单,性能开销小;equals通常需要比较对象的多个属性,性能开销较大

两函数的返回值必须是确定值

在集合中的应用:

1
2
3
4
5
6
HashMap<Person, String> map = new HashMap<>();
HashSet<Person> set = new HashSet<>();

// 当使用HashMap或HashSet时:
// 1. 先调用hashCode确定存储位置
// 2. 如果发生哈希碰撞,再调用equals进行比较

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

参考链接:https://www.51cto.com/article/694975.html?u_atoken=7d59da2167e695040a20985633a6f814&u_asession=01Gimt45TZ3Oy9G0xmWuvr1LteCqj9wXlVbpPqtm1npa9G3yElKR3Z9mEYzBi8yeDzdlmHJsN3PcAI060GRB4YZGyPlBJUEqctiaTooWaXr7I&u_asig=05t-qPxP7Fc-TDkVQ2pHJkMzwmR8hYqDPyzM9kJsZ6r0hVIsGAITQzl0QaI3NwoJc9Hx8o21tQH6OJa1FaAkmhSfOQy9Gj_TEr6pLFdzyOhyIRjiLISPGPwPyX4opgMp9amr1NXaxQw6O4kapaAdo95tjf1LgF4C2gwbnk7IhLd4Vg2QMxYs6lyXb1lFWKql56kGAJWlf8fanqc2SskBnaoK1hxjoMZf8p0Ump5aaJGf76oXRW8u3C7tki6aRF36vr3rwDQZciNcfWMdjBx-qZzDkIiMPAbPUWwCQd7Wsyl_CsTpJ-4hEVCCqo-GZeD3WUZHi7af-9T9DT_5BT1SiXZw&u_aref=sAO9blcJXnIZvLEEHCko3qXscBM%3D

当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。

那为什么不直接使用 hashCode 就确定两个对象是否相等呢?这是因为不同对象的 hashCode 可能相同;但 hashCode 不同的对象一定不相等,所以使用 hashCode 可以起到快速初次判断对象是否相等的作用。

为什么要同时重写呢?是因为如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象。

但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。

6.浅拷贝和深拷贝有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Classroom {
String name; // 教室名称
ArrayList<Desk> desks; // 教室里的桌子们

// 浅拷贝
Classroom shallowCopy() {
Classroom copy = new Classroom();
copy.name = this.name;
copy.desks = this.desks; // 只复制引用,还是指向原来的桌子们
return copy;
}

// 深拷贝
Classroom deepCopy() {
Classroom copy = new Classroom();
copy.name = this.name;
copy.desks = new ArrayList<>(); // 创建新的集合
for (Desk desk : this.desks) {
copy.desks.add(desk.clone()); // 每张桌子都完全复制一份新的
}
return copy;
}
}

简单来说:

  • 浅拷贝:复制对象本身,但里面的对象还是引用原来的
  • 深拷贝:复制对象本身,同时也复制对象内部的所有对象,形成完全独立的新对象

7.深拷贝有几种实现方式?

参考链接:https://juejin.cn/post/6844903693100417038

构造函数

我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。

1
2
// 调用构造函数时进行深拷贝
User copyUser = new User(user.getName(), new Address(address.getCity(), address.getCountry()));

重载clone()方法

Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User implements Cloneable {
private String name; // String是不可变类,不需要特殊处理
private Address address;

@Override
public User clone() throws CloneNotSupportedException {
// 第一步:创建基础的浅拷贝
User user = (User) super.clone();

// 第二步:对引用类型的字段进行深拷贝
user.setAddress(this.address.clone());

return user;
}
}

这就像是一个两步操作:

  1. 先复制”框架”(User对象本身)
  2. 再复制”内容物”(User对象中的引用类型属性)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
步骤1 (super.clone()):
原始User对象 新User对象
┌───┐ ┌───┐
│User│ ──复制──> │User│
└─┬─┘ └─┬─┘
│ │
▼ ▼
Address Address (同一个地址对象)

步骤2 (address.clone()):
原始User对象 新User对象
┌───┐ ┌───┐
│User│ │User│
└─┬─┘ └─┬─┘
│ │
▼ ▼
Address Address (独立的地址对象)

另外,虽然String类也是引用类型,但是由于其特殊的不可变特性,因此也不用new,java会帮我们自动创建新的对象。接下来的两个问题会有提到。

Serializable接口序列化

Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void serializableCopy() {

Address address = new Address("杭州", "中国");
User user = new User("大山", address);

// 使用Apache Commons Lang序列化进行深拷贝
User copyUser = (User) SerializationUtils.clone(user);

// 修改源对象的值
user.getAddress().setCity("深圳");

// 检查两个对象的值不同
assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

8.String、StringBuffer、StringBuilder的区别?

参考链接:https://dunwu.github.io/javacore/pages/bc583c/#string-%E7%9A%84%E4%B8%8D%E5%8F%AF%E5%8F%98%E6%80%A7

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 是一个线程安全的可修改字符序列。StringBuffer 的线程安全是通过在各种修改数据的方法上用 synchronized 关键字修饰实现的。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

StringBufferStringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy

除非有线程安全的需要,不然一般都使用 StringBuilder

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StringBuilderDemo {
public static void main(String[] args) {
// 创建一个StringBuilder对象
StringBuilder sb = new StringBuilder();

// 使用append方法添加不同类型的数据
sb.append("Hello "); // 添加字符串
sb.append("World"); // 继续添加字符串
sb.append('!'); // 添加字符
sb.append("\n"); // 添加换行符
sb.append(2024); // 添加数字
sb.append(" 年\n"); // 添加中文

// 链式调用示例
sb.append("价格: ")
.append(99.9)
.append(" 元")
.append("\n");

// 在指定位置插入内容
sb.insert(6, "beautiful ");

// 输出最终结果
System.out.println("最终字符串内容:");
System.out.println(sb.toString());

// 显示容量和长度
System.out.println("StringBuilder 容量: " + sb.capacity());
System.out.println("字符串长度: " + sb.length());
}
}

9.Java的 String 类为什么不可变?

1.String 类被 final 修饰,是不可变类

2.String 类的数据存储在 char[]数组中,这个数组被 final 修饰,表示 String 对象不可被更改

为什么 Java 要这样设计?

(1)保证 String 对象安全性。避免 String 被篡改。

(2)保证 hash 值不会频繁变更

(3)可以实现字符串常量池