java-双大括号实例初始化的反模式

今天在看springboot的batch时, 看到这样一段代码, 直接把我看懵了, 于是找了一下, 发现这 两个大括号 {{  叫实力初始化器

 FlatFileItemReader<Person> reader = new FlatFileItemReader<>();

        // 设置 csv文件的路径
        reader.setResource(new ClassPathResource("people.csv"));
        // 对cvs文件的数据和领域模型做对应映射
        reader.setLineMapper(new DefaultLineMapper<Person>() {{
            setLineTokenizer(new DelimitedLineTokenizer() {{
                setNames(new String[] {"name", "age", "nation", "address"});
                }});
            setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                setTargetType(Person.class);
            }});
        }});

1, 什么是java的实例初始化器

1, 平时创建map并put值的时候, 是这样操作的

Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);

或者, 在静态代码块中进行初始化

private static final Set<Integer> set = new HashSet<>();
static {
    set.add(1);
    set.add(2);
    set.add(3);
}

或者, 简写成这种形式的

Set<Integer> set = new HashSet<Integer>() {{
    add(1);
    add(2);
    add(3);
}};

于是, 可以作为函数参数传入:

someFunction(new HashSet<Integer>() {{
    add(1);
    add(2);
    add(3);
}}
);

这儿的大括号, 其实就是一段 { } 局部代码的写法:

Set<Integer> set = new HashSet<Integer>() {
    {
        add(1);
        add(2);
        add(3);
    }
};

这样, 不管使用哪一个构造器, 都会执行我们的这段代码

2, 饭模式的内存泄漏

原博客地址: http://deepinmind.iteye.com/blog/2165827

说它是一个反模式主要出于三方面的原因:

1. 可读性

可读性是最次要的一个原因。尽管它使得程序编写变得更简单,并且看起来跟JSON中数据结构的初始化差不多:

{
  "firstName"     : "John"
, "lastName"      : "Smith"
, "organizations" :
  {
    "0"   : { "id", "1234" }
  , "abc" : { "id", "5678" }
  }
}  

2. 一个实例,一种类型

通过一次双括号的初始化我们其实就已经创建了一个新类型了!通过这种方式所生成的每一个新map,都会隐式地创建了一个无法重复使用的新类型。如果仅用一次的话也无可厚非。但如果在一个大型的应用中到处都充斥着这种代码的话,无形中会给你的类加载器增加了许多负担,你的堆会持有着这些类的引用。不信么?编译下上述代码并查看下编译器的输出。大概会是这样的:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class  

这里只有最外围的Test.class是有意义的。

不过这还不是最重要的问题。

3. 内存泄露!

最重要的问题就是匿名类所造成的。它们持有着外围实例的引用,这简直就是个定时ZHADAN。想像一下,你把这个看似NB的HashMap初始化放到一个EJB或者是一个很重的包含着这样的生命周期的对象里:

public class ReallyHeavyObject {  

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;  

    // This method almost does nothing
    public void quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};  

        // Some more code here
    }
}  

这个ReallyHeavyObject类中有许多资源,当ReallyHeavyObject对象被垃圾回收掉时这些资源是需要尽快被释放掉的。不过调用quickHarmlessMethod()方法并不会造成什么影响,因为这个map很快就会被回收掉了。

好的。

我们假设另外一个开发人员,他重构了一下这个方法,返回了这个map,或者是map中的某一部分:

public Map quickHarmlessMethod() {
    Map source = new HashMap(){{
        put("firstName", "John");
        put("lastName", "Smith");
        put("organizations", new HashMap(){{
            put("0", new HashMap(){{
                put("id", "1234");
            }});
            put("abc", new HashMap(){{
                put("id", "5678");
            }});
        }});
    }};  

    return source;
}  

这下问题就严重了!现在你把ReallyHeavyObject中的所有状态都暴露给外部了,因为每个内部类都会持有一个外围实例的引用,也就是ReallyHeavyObject实例。不信么?运行下这段程序看看:

public static void main(String[] args) throws Exception {
    Map map = new ReallyHeavyObject().quickHarmlessMethod();
    Field field = map.getClass().getDeclaredField("this$0");
    field.setAccessible(true);
    System.out.println(field.get(map).getClass());
}  

确实是这样!。如果你仍不相信的话,还可以使用调试器来查看下这个返回的map的内部状态。

你会发现外围实例的引用就在这个匿名的HashMap子类中安静地躺着。所有的这些匿名子类型都会持有一个这样的引用。

因此,不要使用这个反模式

你可能会说,如果将quickHarmlessMethod()声明成static的不就好了,这不会出现3中的泄露问题了,你说的没错。

不过上述代码中最糟糕的问题就是即便你知道这个静态上下文中的map该如何使用,但下一个开发人员可能会注意不到,他还可能会把这个static重构或者删除掉。他们还可能会把这个map存储在一个单例中,这样你就很难再从代码中看出哪里会有一个无用的ReallyHeavyObject的引用。

内部类是一头野兽。它已经造成过许多的问题以及认知失衡。匿名内部类则更为严重,因为读到这段代码的人可能完全没有意识到自己已经包装了一个外围实例,并且把这个实例传递到了别处。

结论便是:

不要自作聪明了,别使用双括号来进行初始化。

时间: 2024-12-21 21:32:16

java-双大括号实例初始化的反模式的相关文章

Java中的常量如何避免反模式

在应用中,我们往往需要一个常量文件,用于存储被多个地方引用的共享常量.在设计应用时,我也遇到了类似的情况,很多地方都需要各种各样的常量. 我确定需要一个单独的文件来存储这些静态公共常量.但是我不是特别确定是应该用接口还是类(枚举不满足我的需求).我有两种选择: 使用接口,如: 1 2 3 4 5 package one; public interface Constants { String NAME="name1"; int MAX_VAL=25; } 或 1 2 3 4 5 pac

Java类、实例初始化的顺序

求如下 java 代码的输出?? class T implements Cloneable{ public static int k = 0; public static T t1 = new T("t1"); public static T t2 = new T("t2"); public static int i = print("i"); public static int n = 99; public int j = print(&quo

java类及实例初始化顺序

1.静态变量.静态代码块初始化顺序级别一致,谁在前,就先初始化谁.从上而下初始化(只在类加载时,初始化一次) 2.非静态变量.非静态代码块初始化顺序级别一致,谁在前,就先初始化谁.从上而下初始化(只要对象实例化一次,就初始化一次) 3.构造方法在非静态变量.非静态代码块之后执行. 4.子类非静态变量.非静态代码块在父类构造方法之后执行. 5.子类构造方法在父类构造方法之后执行. 6.静态方法不会被子类重写. 原文地址:https://www.cnblogs.com/yuefeng123/p/12

对Java中使用两个大括号进行初始化的理解

最近重读Java 编程思想,读到有关实例化代码块儿 的内容,使我对于使用两个大括号进行初始化有了更深的理解. 实例化代码块儿: 和静态代码块儿的概念相对应,静态代码块儿是static 关键字 + 大括号,把静态代码块儿的static关键字去掉就是实例化代码块儿,静态代码块儿在类初始化的时候执行一次 而实例化代码块儿在每次生成对象的时候都会执行(实例化代码块儿会先于构造方法执行). 使用匿名内部类进行初始化:在new 一个对象的时候,小括号后边跟一个大括号 使用匿名内部类 + 实例化代码块儿 =

Java 接口常量反模式及如何定义 Java 常量

初学 Java 的人很不经意间就会把常量定义在接口中,大概唯一的理由是接口不能实例化,而使用接口中定义的常量也是不用附着在实例上的.这主要还是 JDK 本身给我们做了很多这样的榜样, 如  java.io.ObjectStreamConstans,多是出现在 Enum 类型到来之前. 其实 Java 的接口常量是一种反模式,理由如下: 1. 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父,子接口(或实现) 去引用常量是可能不一致的2. 同样的,由于被实现或

Java初始化顺序(静态变量、静态初始化块、实例变量、实例初始化块、构造方法)

1.执行顺序 1.1.一个类中的初始化顺序 (静态变量.静态初始化块)=>(变量.初始化块.构造器). 1.2.两个具有继承关系类的初始化顺序 父类的(静态变量.静态初始化块)=> 子类的(静态变量.静态初始化块)=> 父类的(变量.初始化块.构造器)=> 子类的(变量.初始化块.构造器). 示例如下:(结果见注释) 1 class A { 2 public A() { 3 System.out.println("Constructor A."); 4 } 5

Java 类的实例变量初始化的过程 静态块、非静态块、构造函数的加载顺序

Java 类的实例变量初始化的过程 静态块.非静态块.构造函数的加载顺序 先看一道Java面试题: 1 public class Baset { 2 private String baseName = "base"; 3 // 构造方法 4 public Baset() { 5 callName(); 6 } 7 // 成员方法 8 public void callName() { 9 // TODO Auto-generated method stub 10 System.out.p

【Simple Java】什么是Java实例初始化块

在本篇文章,我将会使用一个例子展示什么是实例变量初始化块,实例初始化块和静态初始化块,然后说明在Java中实例初始化块是如何工作的. 执行顺序 查看下面的代码,你知道哪个先执行吗? package simplejava; class Foo { // instance variable initializer String s = "abc"; // constructor public Foo() { System.out.println("constructor call

Java静态初始化,实例初始化以及构造方法

首先有三个概念需要了解: 一.静态初始化:是指执行静态初始化块里面的内容. 二.实例初始化:是指执行实例初始化块里面的内容. 三.构造方法:一个名称跟类的名称一样的方法,特殊在于不带返回值. 我们先来看一段程序结果: package com; class Book{ public static int booksum=0;//静态变量 static{//这是静态初始化块 print(); System.out.println("this is static block"); } {//