Java中对象传递、返回与不可变对象的深入剖析
1. C++与Java对象传递差异
在C++里,拷贝构造函数是其重要组成部分,它能自动创建对象的本地副本。例如,当你想按值传递对象时,拷贝构造函数就会复制该对象。然而在Java中,我们操作的一切都是引用,虽然也有类似引用的实体,但不能直接像C++那样传递对象。下面通过简单代码说明:
// C++ 示例,伪代码表示拷贝构造函数作用
// class MyClass {
// public:
// MyClass(const MyClass& other) {
// // 复制操作
// }
// };
// MyClass obj1;
// MyClass obj2 = obj1; // 调用拷贝构造函数
// Java 示例
class MyJavaClass {}
MyJavaClass obj1 = new MyJavaClass();
MyJavaClass obj2 = obj1; // 只是引用复制,非对象复制
所以,C++的拷贝构造函数在Java中并不适用,使用时需注意。
2. 只读类与不可变对象
为避免别名带来的不良影响,可创建属于只读类的不可变对象。在这样的类中,没有方法能改变对象的内部状态,所以别名不会产生影响。
Java标准库为所有基本类型提供了“包装”类,如
Integer
类就是不可变对象的简单示例:
//: appendixa:ImmutableInteger.java
// The Integer class cannot be changed.
import java.util.*;
public class ImmutableInteger {
public static void main(String[] args) {
ArrayList v = new ArrayList();
for(int i = 0; i < 10; i++)
v.add(new Integer(i));
// But how do you change the int
// inside the Integer?
}
} ///:~
Integer
类没有允许改变对象的方法。若需要能修改基本类型的对象,可自行创建,如下:
//: appendixa:MutableInteger.java
// A changeable wrapper class.
import java.util.*;
class IntValue {
int n;
IntValue(int x) { n = x; }
public String toString() {
return Integer.toString(n);
}
}
public class MutableInteger {
public static void main(String[] args) {
ArrayList v = new ArrayList();
for(int i = 0; i < 10; i++)
v.add(new IntValue(i));
System.out.println(v);
for(int i = 0; i < v.size(); i++)
((IntValue)v.get(i)).n++;
System.out.println(v);
}
} ///:~
若默认初始化为零足够,且不关心打印输出,
IntValue
类可更简单:
class IntValue { int n; }
3. 创建只读类示例
下面是创建自己的只读类的示例:
//: appendixa:Immutable1.java
// Objects that cannot be modified
// are immune to aliasing.
public class Immutable1 {
private int data;
public Immutable1(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable1 quadruple() {
return new Immutable1(data * 4);
}
static void f(Immutable1 i1) {
Immutable1 quad = i1.quadruple();
System.out.println("i1 = " + i1.read());
System.out.println("quad = " + quad.read());
}
public static void main(String[] args) {
Immutable1 x = new Immutable1(47);
System.out.println("x = " + x.read());
f(x);
System.out.println("x = " + x.read());
}
} ///:~
该类所有数据都是私有的,公共方法不会修改数据。
quadruple()
方法看似修改对象,实则创建新的
Immutable1
对象,原对象不变。所以,
Immutable1
对象可被多次别名化而无害。
4. 不可变性的缺点及解决方案
创建不可变类起初看似是优雅的解决方案,但需要修改对象时,需承担新对象创建的开销,还可能导致更频繁的垃圾回收。对于某些类,这不是问题,但对于像
String
类,成本过高。
解决方案是创建可修改的伴随类。例如:
//: appendixa:Immutable2.java
// A companion class for making
// changes to immutable objects.
class Mutable {
private int data;
public Mutable(int initVal) {
data = initVal;
}
public Mutable add(int x) {
data += x;
return this;
}
public Mutable multiply(int x) {
data *= x;
return this;
}
public Immutable2 makeImmutable2() {
return new Immutable2(data);
}
}
public class Immutable2 {
private int data;
public Immutable2(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable2 add(int x) {
return new Immutable2(data + x);
}
public Immutable2 multiply(int x) {
return new Immutable2(data * x);
}
public Mutable makeMutable() {
return new Mutable(data);
}
public static Immutable2 modify1(Immutable2 y){
Immutable2 val = y.add(12);
val = val.multiply(3);
val = val.add(11);
val = val.multiply(2);
return val;
}
// This produces the same result:
public static Immutable2 modify2(Immutable2 y){
Mutable m = y.makeMutable();
m.add(12).multiply(3).add(11).multiply(2);
return m.makeImmutable2();
}
public static void main(String[] args) {
Immutable2 i2 = new Immutable2(47);
Immutable2 r1 = modify1(i2);
Immutable2 r2 = modify2(i2);
System.out.println("i2 = " + i2.read());
System.out.println("r1 = " + r1.read());
System.out.println("r2 = " + r2.read());
}
} ///:~
modify1()
方法在
Immutable2
类内完成所有操作,创建四个新对象;
modify2()
方法先将
Immutable2
对象转换为
Mutable
对象,进行大量修改操作,最后再转换回
Immutable2
对象,只创建两个新对象。这种方法适用于以下情况:
1. 需要不可变对象。
2. 经常需要进行大量修改。
3. 创建新的不可变对象成本高。
5. 不可变字符串
String
类是不可变的,如以下代码:
//: appendixa:Stringer.java
public class Stringer {
static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = new String("howdy");
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
} ///:~
当
q
传入
upcase()
方法时,只是引用的副本,原对象不变。
upcase()
方法返回新对象的引用,原
q
不受影响。
6. 隐式常量
在代码中,通常不希望方法修改参数,因为参数一般被视为提供给方法的信息。在C++中,使用
const
关键字确保引用不修改原对象,但使用时需注意。而Java没有类似C++的
const
特性,若需要对象不可变,需自行实现,如
String
类。
7. 重载‘+’与
StringBuffer
String
类对象不可变,使用
+
和
+=
操作符拼接字符串时,若每次都创建新
String
对象,效率较低。因此,Java引入了
StringBuffer
类,编译器在处理包含
+
和
+=
的字符串表达式时,会自动创建
StringBuffer
对象。示例如下:
//: appendixa:ImmutableStrings.java
// Demonstrating StringBuffer.
public class ImmutableStrings {
public static void main(String[] args) {
String foo = "foo";
String s = "abc" + foo +
"def" + Integer.toString(47);
System.out.println(s);
// The "equivalent" using StringBuffer:
StringBuffer sb =
new StringBuffer("abc"); // Creates String!
sb.append(foo);
sb.append("def"); // Creates String!
sb.append(Integer.toString(47));
System.out.println(sb);
}
} ///:~
虽然使用
StringBuffer
更高效,但创建引号字符字符串时,编译器仍会将其转换为
String
对象,可能创建比预期更多的对象。
8.
String
与
StringBuffer
类方法概述
以下是
String
和
StringBuffer
类的重要方法概述:
| 类 | 方法 | 参数、重载情况 | 用途 |
| ---- | ---- | ---- | ---- |
| String | 构造函数 | 重载:默认、String、StringBuffer、字符数组、字节数组 | 创建String对象 |
| String | length() | 无 | 字符串中的字符数 |
| String | charAt() | int Index | 字符串中指定位置的字符 |
| String | getChars()、getBytes() | 开始和结束位置、要复制到的数组、目标数组的索引 | 将字符或字节复制到外部数组 |
| String | toCharArray() | 无 | 生成包含字符串中字符的char[] |
| String | equals()、equalsIgnoreCase() | 要比较的String | 检查两个字符串内容是否相等 |
| String | compareTo() | 要比较的String | 根据字典顺序返回负、零或正值 |
| String | regionMatches() | 偏移量、另一个String及其偏移量和长度,重载可忽略大小写 | 检查区域是否匹配 |
| String | startsWith() | 可能的起始字符串,重载可指定偏移量 | 检查字符串是否以指定字符串开头 |
| String | endsWith() | 可能的后缀字符串 | 检查字符串是否以指定字符串结尾 |
| String | indexOf()、lastIndexOf() | 重载:字符、字符和起始索引、String、String和起始索引 | 查找参数在字符串中的位置,未找到返回 -1 |
| String | substring() | 重载:起始索引、起始索引和结束索引 | 返回包含指定字符集的新String对象 |
| String | concat() | 要连接的String | 返回连接后的新String对象 |
| String | replace() | 要查找的旧字符、要替换的新字符 | 返回替换后的新String对象 |
| String | toLowerCase()、toUpperCase() | 无 | 返回大小写转换后的新String对象 |
| String | trim() | 无 | 返回去除首尾空格后的新String对象 |
| String | valueOf() | 重载:Object、char[]、char[]和偏移量及计数、boolean、char、int、long、float、double | 返回包含参数字符表示的String |
| String | intern() | 无 | 为每个唯一字符序列生成一个且仅一个String引用 |
| StringBuffer | 构造函数 | 重载:默认、要创建的缓冲区长度、要从中创建的String | 创建新的StringBuffer对象 |
| StringBuffer | toString() | 无 | 从StringBuffer创建String |
| StringBuffer | length() | 无 | StringBuffer中的字符数 |
| StringBuffer | capacity() | 无 | 返回当前分配的空间数 |
| StringBuffer | ensureCapacity() | 期望的容量 | 确保StringBuffer至少能容纳指定数量的空间 |
| StringBuffer | setLength() | 新的字符串长度 | 截断或扩展前一个字符串,扩展时用空字符填充 |
| StringBuffer | charAt() | 所需元素的位置 | 返回缓冲区中该位置的字符 |
| StringBuffer | setCharAt() | 所需元素的位置和新的字符值 | 修改该位置的值 |
| StringBuffer | getChars() | 开始和结束位置、要复制到的数组、目标数组的索引 | 将字符复制到外部数组 |
| StringBuffer | append() | 重载:Object、String、char[]、char[]和偏移量及长度、boolean、char、int、long、float、double | 将参数转换为字符串并追加到当前缓冲区末尾 |
| StringBuffer | insert() | 重载,第一个参数为插入起始偏移量,第二个参数为要插入的内容 | 将参数转换为字符串并插入到当前缓冲区指定位置 |
| StringBuffer | reverse() | 无 | 反转缓冲区中字符的顺序 |
9. 总结
在Java中,一切都是引用,对象在堆上创建,不再使用时会被垃圾回收,这改变了对象操作方式。传递和返回对象时,只需传递引用,无需担心对象的生命周期和责任问题,简化了编程。但也存在两个缺点:
1. 额外的内存管理会影响效率,且垃圾回收时间不确定。不过,对于大多数应用,好处大于缺点,时间关键部分可使用本地方法。
2. 别名问题,可能意外出现两个引用指向同一对象的情况。可使用
clone()
方法或创建不可变对象解决。
有些人认为Java的克隆设计不佳,会自行实现克隆方法,避免调用
Object.clone()
方法,从而无需实现
Cloneable
接口和捕获
CloneNotSupportedException
异常。
下面是一个简单的mermaid流程图,展示
modify2()
方法的执行流程:
graph TD;
A[开始] --> B[获取Immutable2对象y];
B --> C[将y转换为Mutable对象m];
C --> D[m进行多次修改操作];
D --> E[m转换回Immutable2对象];
E --> F[返回结果Immutable2对象];
F --> G[结束];
通过以上内容,我们深入了解了Java中对象传递、返回、不可变对象以及相关类的使用和特性,在实际编程中可根据需求合理选择使用。
Java中对象传递、返回与不可变对象的深入剖析
10. 应用场景分析
在实际编程中,我们需要根据不同的场景来选择使用不可变对象、可变对象以及它们的伴随类。以下是一些具体的应用场景分析:
10.1 频繁读取但很少修改的场景
当我们的程序需要频繁读取某个对象的值,但很少对其进行修改时,使用不可变对象是一个不错的选择。例如,在配置信息管理系统中,配置信息在程序启动后通常不会发生变化,此时可以将配置信息封装为不可变对象。以下是一个简单的示例:
// 不可变配置类
final class Configuration {
private final String host;
private final int port;
public Configuration(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
public class ConfigExample {
public static void main(String[] args) {
Configuration config = new Configuration("localhost", 8080);
// 频繁读取配置信息
System.out.println("Host: " + config.getHost());
System.out.println("Port: " + config.getPort());
}
}
10.2 大量修改操作的场景
如果程序中需要对某个对象进行大量的修改操作,使用可变对象或不可变对象的伴随类会更高效。例如,在数据统计系统中,需要对统计数据进行不断的累加和更新,此时可以使用可变对象。以下是一个简单的示例:
// 可变统计类
class Statistics {
private int total;
public Statistics() {
this.total = 0;
}
public void add(int value) {
total += value;
}
public int getTotal() {
return total;
}
}
public class StatisticsExample {
public static void main(String[] args) {
Statistics stats = new Statistics();
// 大量修改操作
for (int i = 0; i < 100; i++) {
stats.add(i);
}
System.out.println("Total: " + stats.getTotal());
}
}
11. 性能优化建议
在使用不可变对象和可变对象时,我们可以采取一些性能优化措施,以提高程序的运行效率。以下是一些建议:
11.1 合理使用不可变对象
虽然不可变对象具有线程安全、易于理解和维护等优点,但创建新对象会带来一定的开销。因此,在需要频繁创建和修改对象的场景中,应谨慎使用不可变对象。可以在对象创建后很少修改的情况下使用不可变对象,以减少对象创建和垃圾回收的开销。
11.2 利用可变对象进行批量操作
在需要进行大量修改操作的场景中,使用可变对象可以避免频繁创建新对象,从而提高性能。可以先使用可变对象进行批量修改,最后再将其转换为不可变对象。例如,在
Immutable2
类的示例中,
modify2()
方法先将
Immutable2
对象转换为
Mutable
对象进行修改,最后再转换回
Immutable2
对象,减少了新对象的创建。
11.3 注意字符串拼接
在进行字符串拼接时,应尽量使用
StringBuffer
或
StringBuilder
类,避免使用
+
和
+=
操作符。因为
String
类是不可变的,使用
+
和
+=
操作符会创建大量的临时对象,影响性能。
StringBuffer
是线程安全的,而
StringBuilder
是非线程安全的,在单线程环境中,使用
StringBuilder
会更高效。以下是一个示例:
public class StringConcatenationExample {
public static void main(String[] args) {
// 使用 StringBuilder 进行字符串拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
String result = sb.toString();
System.out.println(result);
}
}
12. 常见问题解答
在使用不可变对象和可变对象的过程中,可能会遇到一些常见问题,以下是一些解答:
12.1 如何判断一个类是否是不可变的?
一个类如果满足以下条件,则可以认为是不可变的:
1. 所有字段都是
private
和
final
的。
2. 没有提供修改字段值的方法。
3. 不允许子类重写可能修改对象状态的方法(可以将类声明为
final
)。
12.2 不可变对象和线程安全有什么关系?
不可变对象是线程安全的,因为它们的状态在创建后不能被修改。多个线程可以同时访问不可变对象,而不会出现数据竞争和不一致的问题。因此,在多线程环境中,使用不可变对象可以避免同步问题,提高程序的并发性能。
12.3 如何在不可变对象和可变对象之间进行转换?
可以通过创建伴随类的方式来实现不可变对象和可变对象之间的转换。例如,在
Immutable2
类的示例中,
Mutable
类是
Immutable2
类的伴随类,通过
makeMutable()
和
makeImmutable2()
方法可以实现两者之间的转换。
13. 总结与展望
通过本文的介绍,我们深入了解了Java中对象传递、返回、不可变对象以及相关类的使用和特性。不可变对象在避免别名问题、保证线程安全等方面具有重要作用,但也存在创建新对象开销大的问题。可变对象则适用于需要进行大量修改操作的场景。在实际编程中,我们应根据具体需求合理选择使用不可变对象和可变对象,并采取相应的性能优化措施。
未来,随着Java技术的不断发展,对象的管理和使用可能会更加高效和灵活。例如,Java可能会引入更高级的内存管理机制,减少对象创建和垃圾回收的开销。同时,也可能会提供更多的工具和方法来帮助开发者更好地处理不可变对象和可变对象。
以下是一个mermaid流程图,展示了在不同场景下选择不可变对象和可变对象的决策过程:
graph TD;
A[开始] --> B{是否需要频繁修改对象};
B -- 是 --> C[使用可变对象或伴随类];
B -- 否 --> D{是否需要线程安全};
D -- 是 --> E[使用不可变对象];
D -- 否 --> F[根据其他需求选择];
C --> G[进行修改操作];
E --> H[进行读取操作];
F --> I[根据具体情况选择];
G --> J[结束];
H --> J[结束];
I --> J[结束];
通过合理运用不可变对象和可变对象,我们可以编写出更加高效、安全和易于维护的Java程序。希望本文对您在Java编程中有所帮助。
超级会员免费看
170万+

被折叠的 条评论
为什么被折叠?



