Java常见细节

本文最后更新于:2021年5月15日 晚上

Java常见细节

1. 「replace会替换所有字符?」

如果将字符串中 A 替换为 B ,则使用 replaceAll 方法。

replace 会替换所有匹配字符吗?

Replaces each substring of this string that matches the literal target sequence with the specified literal replacement sequence. The replacement proceeds from the beginning of the string to the end, for example, replacing “aa” with “b” in the string “aaa” will result in “ba” rather than “ab”.

  • 替换从字符串的开头一直进行到结尾,匹配成功就替换,然后接下去。
  • 例如:原字符串:aaa ,将 aa 替换为 b ,则结果是 ba 而不是 ab ,因为是从前往后。
  1. replace 两个重载方法
1
public String replace(char oldChar, char newChar) /* 替换一个字符 */
1
public String replace(CharSequence target, CharSequence replacement) /* 替换一个串 */
  1. replaceAll
1
public String replaceAll(String regex, String replacement) /* 前面正则表达式,匹配成功替换为后面字符串 */

举例🌰:原串 s 里的 * 替换为 A 两种写法:

  • s.replaceAll("\\*", "A") 只能接正则表达式匹配出来替换。
  • s.replace("*", "A")
  1. replaceFirst 只替换第一个匹配字符串
1
public String replaceFirst(String regex, String replacement)

2. 「Integer不能用 == 判断?」

1
2
3
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);
  • 结果会返回 false
  • 不是说 Integer 有缓存吗,范围是 -128 ~ 127
1
2
3
public Integer(int value) {
this.value = value;
}
  • 有参构造并没有用到缓存
1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
  • 要用到缓存,就应该使用 valueOf 方法
1
2
3
String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));
  • 这样就会返回 true
  • 只有这种特殊情况才会使判断结果相同,养成好喜欢,统一使用 equals 来判断相等。

3. 「使用BigDecimal不丢失精度?」

涉及金融业务,不可避免接触到小数。

使用Double时出现的场景:

1
2
3
double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1); // 0.009999999999999998

结果小于预计结果。

  • Double类型两个参数相减会转换为二进制,而Double有效位是16位,出现存储小数位不够。
  • 使用 BigDecimal 就可以避免丢失精度吗?还是要看你怎么用。
1
2
3
BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

0.0099999999999999984734433411404097569175064563751220703125

可以看到仍然丢失精度。

BigDecimal 带 double 参数的构造方法上的注释

Notes:

  1. The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

  2. The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal(“0.1”) creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

  3. When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

  • public BigDecimal(double val) 这种构造方法生成的结果不可预测(unpredictable)

  • 注释举例的 0.1 无法准确替换为double,即无法替换为任何有限长度的二进制分数

  • 官方推荐入参选用 String 字符串,这样传入多少,生成多少。

  • 如果非得用 double 入参,则先 Double.toString(double) 转成String再构造。

  • 或者使用 public static BigDecimal valueOf(double val) 静态方法生成,也是阿里巴巴开发手册推荐的构造形式。

4. 「字符串拼接不能用String?」

String类型的字符串被称为不可变序列,在大量字符串拼接的场景中,如果对象定义为 String ,会产生很多无用的中间对象,浪费内存空间,效率低。

推荐使用更高效的可变字符序列:StringBuilderStringBuffer

StringBuilder 虽然线程不安全,但是多线程拼接字符串情况很少,所以就使用这个对象。append 方法追加字符串即可。

「字符串拼接时使用String类型的对象,效率一定比StringBuilder类型的对象低?」

否。

通过 javap -c 反编译可知,总JDK5开始,java对String拼接字符串进行优化,编译成字节码都会变成StringBuilder的append操作。

5. 「Map初始化注意事项」

  • Map初始化同时导入数据有以下三种常见方法
  1. static块初始化
1
2
3
4
5
6
7
8
9
public class Solution {
private static final Map<String, String> myMap;

static {
myMap = new HashMap<String, String>();
myMap.put("a", "b");
myMap.put("c", "d");
}
}
  1. Guava类库
1
2
3
4
5
6
7
8
9
// 创建一个HashMap
Map<String, Object> hashMap = Maps.newHashMap();
// 创建不可变Map
Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
// 或者
Map<String, String> test = ImmutableMap.<String, String>builder()
.put("k1", "v1")
.put("k2", "v2")
.build();
  1. 使用匿名内部类
1
2
3
4
5
6
7
public class Solution {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
}
  • 📌外层的括号是创建 匿名内部类 「Anonymous Inner Class」 ,这里会造成隐患
  • 内层是括号是创建 实例初始化块

①验证匿名内部类

  • 首先 javac 编译 .java 文件
1
2
3
4
5
6
7
public class Solution {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
}

1
2
3
4
5
6
7
8
class Solution$1 extends HashMap {
Solution$1(Solution var1) {
this.this$0 = var1;
this.put("map1", "value1");
this.put("map2", "value2");
this.put("map3", "value3");
}
}
  • 说明的确是创建了一个匿名内部类

②非静态内部类持有外部类引用

  • Java 中非静态内部类持有外部类的引用,从而导致 GC 无法回收这部分代码的引用,以至于造成内存溢出
  1. 🍬为什么要持有外部类

非静态匿名内部类的主要作用有两个。

  • 当匿名内部类只在外部类(主类)中使用时,匿名内部类可以让外部不知道它的存在,从而减少了代码的维护工作。

  • 当匿名内部类持有外部类时,它就可以直接使用外部类中的变量了,这样可以很方便的完成调用:

1
2
3
4
5
6
7
8
9
10
11
public class Solution {
private static String userName = "hypocrite30";
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
put(userName, userName);
}};
}
}
  • 内部类 HashMap 可以直接调用外部类 Solution 中的成员变量 userName.
  1. 🍬它是怎么持有外部类的?
  • javap -c '.\Solution$1.class' 反编译内部类
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
class Solution$1 extends java.util.HashMap {
final Solution this$0;

Solution$1(Solution);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LSolution;
5: aload_0
6: invokespecial #2 // Method java/util/HashMap."<init>":()V
9: aload_0
10: ldc #3 // String map1
12: ldc #4 // String value1
14: invokevirtual #5 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
17: pop
18: aload_0
19: ldc #6 // String map2
21: ldc #7 // String value2
23: invokevirtual #5 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
26: pop
27: aload_0
28: ldc #8 // String map3
30: ldc #9 // String value3
32: invokevirtual #5 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
35: pop
36: return
}
  • Code2:putfield :一对 Solution 的引用被存入 this$0 中,此时该匿名内部类持有外部类的引用

  • 另一种直观测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Map map = new Solution().createMap();
Field field = map.getClass().getDeclaredField("this$0");
field.setAccessible(true);
System.out.println(field.get(map).getClass());
}

public Map createMap() {
Map<String, String> map = new HashMap<>() {{
put("k1", "v1");
put("k2", "v2");
}};
return map;
}
}

执行结果:Solution

结论:匿名内部类持有外部类的引用,所以使用 $0 可以正常获取外部类,输入相关信息。

③匿名内部类隐患

  • 正常代码:
1
2
3
4
5
6
7
8
public void createMap() {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
// 业务逻辑
}
  • 转换成这样就有可能造成内存泄漏
1
2
3
4
5
6
7
8
public Map createMap() {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
return map;
}

因为当此 map赋值为其他类属性时,可能会导致 GC 收集时不清理此对象,这时候才会导致内存泄漏

非静态匿名内部类持有外部类引用,因此串行化这个集合时外部类也会被串行化,当外部类没有实现serialize接口时,就会报错。

  • map 对象声明为 static 静态类型就不会泄漏内存
1
2
3
4
5
6
7
8
public static Map createMap() {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
return map;
}
  • 反编译查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution$1 extends java.util.HashMap {
Solution$1();
Code:
0: aload_0
1: invokespecial #1 // Method java/util/HashMap."<init>":()V
4: aload_0
5: ldc #2 // String map1
7: ldc #3 // String value1
9: invokevirtual #4 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
12: pop
13: aload_0
14: ldc #5 // String map2
16: ldc #6 // String value2
18: invokevirtual #4 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: aload_0
23: ldc #7 // String map3
25: ldc #8 // String value3
27: invokevirtual #4 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
30: pop
31: return
}
  • 没有 putfield 关键字,静态匿名类不会持有外部对象引用
  • 因为匿名内部类是静态的之后,它所引用的对象或属性也必须是静态的了,因此就可以直接从 JVM 的 Method Area(方法区)获取到引用而无需持久外部对象了
  • 尽管如此还是不建议使用双括号方式初始map

④替代方案

  1. Stream
  • Java8 的 Stream API
1
2
3
4
5
6
List list = new ArrayList() {{
add("Java");
add("Redis");
}};
// 替换为↓
List list = Stream.of("Java", "Redis").collect(Collectors.toList());
  1. Java9集合工厂
1
2
3
4
5
6
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
}};
// 替换为↓
Map map = Map.of("map1", "Java", "map2", "Redis");

鸣谢:https://www.cnblogs.com/vipstone/p/12937582.html


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!