Java中如何使用匿名内部类?

320
在Java中,匿名类有什么用途?我们可以说使用匿名类是Java的一个优点之一吗?

68
这并不是Java的优势,而是一种解决Java缺乏闭包的方法。 - Eric Wilson
5
Java 8引入了Lambda表达式。请查阅答案链接:https://dev59.com/bXRC5IYBdhLWcg3wROzk#40650596 - akhil_mittal
19个回答

377

通过"匿名类",我认为你指的是匿名内部类

当需要创建一个带有某些“附加功能”的对象实例(例如重写方法),而不必实际子类化类时,可以使用匿名内部类。

我倾向于将其用作快捷方式来附加事件监听器:

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // do something
    }
});
使用这种方法可以使编码稍微快一点,因为我不需要制作一个实现ActionListener的额外类 - 我只需实例化一个匿名内部类而不实际制作一个单独的类。我仅在执行“快速且简单”的任务时使用此技术,在这种情况下,制作整个类似乎是不必要的。具有执行完全相同操作的多个匿名内部类应重构为实际类,无论是内部类还是单独类。

5
或者,您可以将重复的匿名内部类重构为具有匿名内部类的一个方法(并可能包含其他重复代码)。 - Tom Hawtin - tackline
3
很好的回答,但是我有一个快速的问题。这是否意味着Java可以没有匿名内部类,它们只是额外可以选择的选项? - realPK
5
讲解得非常清楚,不过我建议任何阅读此文的人都去查找一下Java 8和Lambda表达式能够做什么,以使编码更快、更易读。 - Pievis
2
@user2190639,确切地说,使用Java8中的Lambda表达式无法更好了。 - bonCodigo
3
你为什么说“重载方法”而不是“覆盖方法”? - Tarun
显示剩余3条评论

78

匿名内部类实际上是闭包,因此它们可以用来模拟lambda表达式或“委托”。例如,考虑以下接口:

public interface F<A, B> {
   B f(A a);
}

你可以匿名使用这个方法在Java中创建一个一等函数。假设你有以下方法,它返回给定列表中大于i的第一个数字,如果没有数字大于i,则返回i:
public static int larger(final List<Integer> ns, final int i) {
  for (Integer n : ns)
     if (n > i)
        return n;
  return i;
}

然后你还有另一种方法,返回给定列表中小于i的第一个数字,如果没有数字比i小,则返回i:

public static int smaller(final List<Integer> ns, final int i) {
   for (Integer n : ns)
      if (n < i)
         return n;
   return i;
}

这些方法几乎是相同的。使用一等函数类型F,我们可以将它们重写为一个方法,如下所示:

public static <T> T firstMatch(final List<T> ts, final F<T, Boolean> f, T z) {
   for (T t : ts)
      if (f.f(t))
         return t;
   return z;
}

您可以使用匿名类来使用firstMatch方法:

F<Integer, Boolean> greaterThanTen = new F<Integer, Boolean> {
   Boolean f(final Integer n) {
      return n > 10;
   }
};
int moreThanMyFingersCanCount = firstMatch(xs, greaterThanTen, x);

这只是一个非常牵强的例子,但很容易看出将函数像值一样传递的能力是一个非常有用的功能。请参见Joel本人的文章“你的编程语言能做到这一点吗?”

一个很好的Java编程库可以用这种风格:Functional Java.


20
不幸的是,我认为在Java中进行函数式编程时过于冗长,其代价大于收益。函数式编程的显著特点之一是它倾向于减少代码量,使事情更易于阅读和修改。但是函数式Java似乎并没有做到这一点。 - Chii
29
函数式编程的易懂性,加上Java的简洁性! - Adam Jaskiewicz
3
在我的经验中,使用Java的函数式编程风格需要在前期付出冗长的代码代价,但从长远来看可以得到简洁的代码收益。例如,myList.map(f)比相应的for循环要简洁得多。 - Apocalisp
2
Scala是一种函数式编程风格的语言,据说在JVM内运行良好,可能是函数式编程场景的选择。 - Darrell Teague

57

匿名内部类在以下场景中使用:

1.) 当类定义不可重用且只适用于当前情况时,用于重写(子类化):

class A{
    public void methodA() {
        System.out.println("methodA");
    }
}

class B{
    A a = new A() {
        public void methodA() {
            System.out.println("anonymous methodA");
        }
    };
}

2.) 对于实现接口,仅在当前情况下需要实现接口:

interface InterfaceA{
    public void methodA();
}

class B{
    InterfaceA a = new InterfaceA() {
        public void methodA() {
            System.out.println("anonymous methodA implementer");
        }
    };
}

3.) 参数定义匿名内部类:

interface Foo {
    void methodFoo();
}

class B{
    void do(Foo f) { }
}

class A{
    void methodA() {
        B b = new B();
        b.do(new Foo() {
            public void methodFoo() {
                System.out.println("methodFoo");
            } 
        });
    } 
} 

10
好的,看起来第三种模式是用于事件监听器的。 - xdl
好的解释。 - Vishwa Ratna

49

有时我会将它们用作 Map 实例化的语法技巧:

Map map = new HashMap() {{
   put("key", "value");
}};
vs
Map map = new HashMap();
map.put("key", "value");

在进行许多put语句时,这可以节省一些重复性工作。但是,当外部类需要通过远程传输进行序列化时,我也遇到了问题。


58
要明确的是,第一组大括号是匿名内部类(继承HashMap)。第二组大括号是实例初始化程序(而不是静态初始化程序),然后在您的HashMap子类上设置值。+1提到它,-1没有为新手详细解释。;-D - Spencer Kormos
4
在这里阅读有关双大括号语法的更多信息。 - Martin Andersson
2
http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/ - starcorn

18

它们通常被用作冗长的回调形式。

我想你可以说相比没有它们并且每次都需要创建一个命名类,它们是一种优势,但是类似的概念在其他语言中实现得更好(例如作为闭包或块)

这里有一个swing示例

myButton.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent e) {
        // do stuff here...
    }
});

虽然还是有点杂乱冗长,但是这比你强制为每一个一次性的监听器定义一个命名类要好得多(尽管根据情况和重用情况,仍可能是更好的方法)


1
你是不是想说“简洁”呢?如果使用冗长格式,回调函数将会独立存在,并且变得有点长,因此会变得冗长。如果你认为这仍然很冗长,那么什么样的精练格式才是适当的呢? - user3081519
1
@user3081519,类似于myButton.addActionListener(e -> { /* do stuff here */})或者myButton.addActionListener(stuff)这样的写法会更加简洁。 - Samuel Edwin Ward

8
您可以在需要在另一个函数内创建特定目的的类的情况下使用它,例如作为监听器、可运行对象(以生成线程)等。
其思想是从函数代码内部调用它们,因此您不需要在其他地方引用它们,也就不需要给它们命名。编译器只会枚举它们。
它们本质上是语法糖,通常应该将它们移到其他地方,因为它们变得越来越大。
虽然我不确定这是否是Java的优点之一,但如果您使用它们(不幸的是我们都经常使用它们),那么您可以认为它们是其中之一。

6

匿名类的指南。

  1. 匿名类同时声明和初始化。

  2. 匿名类必须扩展或实现一个且仅一个类或接口。

  3. 由于匿名类没有名称,因此只能使用一次。

例如:

button.addActionListener(new ActionListener(){

            public void actionPerformed(ActionEvent arg0) {
        // TODO Auto-generated method stub

    }
});

关于#3:并非完全正确。您可以使用反射获取匿名类的多个实例,例如 ref.getClass().newInstance() - icza
规则并不能回答问题。 - user207421

5

是的,匿名内部类绝对是Java的优势之一。

使用匿名内部类,您可以访问周围类的final和成员变量,这在监听器等方面非常有用。

但一个主要的优点是,内部类代码(至少应该)与周围的类/方法/块紧密耦合,并具有特定的上下文(周围的类、方法和块)。


1
拥有访问周围类的能力非常重要!我认为这就是在许多情况下使用匿名类的原因,因为它需要/使用周围类/方法的非公共属性、方法和局部变量,否则(如果使用单独的类)将不得不传递或发布。 - icza

5
new Thread() {
        public void run() {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                System.out.println("Exception message: " + e.getMessage());
                System.out.println("Exception cause: " + e.getCause());
            }
        }
    }.start();

这也是使用线程的匿名内部类型的示例之一。

4

一个内部类与外部类的实例相关联,有两种特殊类型:局部类和匿名类。匿名类使我们能够同时声明和实例化一个类,从而使代码更加简洁。当我们只需要一个局部类一次时,我们使用它们,因为它们没有名称。

考虑来自文档的示例,其中我们有一个Person类:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

我们有一种方法来打印符合搜索条件的成员,如下:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

其中CheckPerson是一个类似于接口的东西:

interface CheckPerson {
    boolean test(Person p);
}

现在,我们可以利用实现此接口的匿名类来指定搜索条件,如下所示:
printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

这里的界面非常简单,匿名类的语法似乎笨拙而不清晰。
Java 8引入了一个术语函数式接口,它是仅有一个抽象方法的接口,因此我们可以说CheckPerson是一个函数式接口。我们可以利用Lambda表达式,将函数作为方法参数传递:
printPersons(
                roster,
                (Person p) -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25
        );

我们可以使用标准的函数式接口Predicate来替换接口CheckPerson,这将进一步减少所需代码量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接