Java 8 Lambda 表达式:模拟 Mixin 实现类的多重继承(Java 8: Now You Have Mixins?)

编程语言 William 266浏览 0评论

Java 8 starts to emerge. It comes with a full new feature: functional programming with the lambda expressions. Here I’ll discuss about a feature that is part of the Lambda project (JSR-335): the virtual extension methods, also called public defender methods. This feature will help you to provide a default implementation for the methods that you have declared in your interfaces. This allows, for example, the Java team to add method declarations in existing interfaces, like List or Map. On their side, the developers do not need to redefine their different implementations in Java libraries (like Hibernate). Thus, Java 8 will be theoritically compatible with existing libraries.

The virtual extension methods are a way to provide multiple inheritance in Java. The team who works on Lambda project argues that the kind of multiple inheritance provided with virtual extension methods is limited to behaviour inheritance. As a challenge, I show that with virtual extension methods, you can also emulate state inheritance. To do so, I describe in this article an implementation of mixin in Java 8.

Java 8 开始出现,带来一个全新特性:使用 Lambda 表达式 (JSR-335) 进行函数式编程。今天我们要讨论的是 Lambda 的其中一部分:虚拟扩展方法,也叫做公共辩护(defender)方法。该特性可以让你在接口定义中提供方法的默认实现。例如你可以为已有的接口(如 List 和 Map)声明一个方法定义,这样其他开发者就无需重新实现这些方法,有点像抽象类,但实际却是接口。当然,Java 8 理论上还是兼容已有的库。

虚拟扩展方法为 Java 带来了多重继承的特性,尽管该团队声称与多重继承不同,虚拟扩展方法被限制用于行为继承。或许通过这个特性你可以看到了多重继承的影子。但你还是可以模拟实例状态的继承。我将在接下来的文章详细描述 Java 8 中通过 mixin 混入实现状态的继承。

What a mixin is?

The mixins are kind of composable abstract classes. They are used in a multi-inheritance context to add services to a class. The multi-inheritance is used to compose your class with as many mixins as you want. For example, if you have a class to represent houses, you can create your house from this class and extend it by inheriting from classes like Garage and Garden. Here is this example written in Scala:

val myHouse = new House with Garage with Garden

To inherit from a mixin is not really a specialization. It is rather a mean to collect functionalities and add them to a class. So, with mixins, you have a mean to do structural factorization of code in OOP and to enhance the readability of classes.

For example, here is a use of mixins in Python in the module socketserver. Here, the mixins help to declare four socket based servers: a UPD and a TCP services using multi-processes, a UPD and a TCP services using multi-threading.

class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass

class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

什么是混入 mixin?

混入是一种组合的抽象类,主要用于多继承上下文中为一个类添加多个服务,多重继承将多个 mixin 组合成你的类。例如,如果你有一个类表示“马”,你可以实例化这个类来创建一个“马”的实例,然后通过继承像“车库”和“花园”来扩展它,使用 Scala 的写法就是:

val myHouse = new House with Garage with Garden

从 mixin 继承并不是一个特定的规范,这只是用来将各种功能添加到已有类的方法。在 OOP 中,有了 mixin,你就有通过它来提升类的可读性。

例如在 Python 的  socketserver 模块中就有使用 mixin 的方法,在这里,mixin 帮助 4 个基于不同 Socket 的 服务,包括支持多进程的 UDP 和 TCP 服务以及支持多线程的 UDP 和 TCP 服务。

class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass

class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
What are virtual extension methods?

Java 8 will introduce the notion of virtual extension method, also called public defender method. Let’s called them VEM.

A VEM aims to provide a default implementation for a method declared in a Java interface. This helps to add a new method declaration in some widely used interfaces, like the interfaces of the JDK’s Collection API. Thus, all the existing libraries like Hibernate that reimplements the Collection API do not need to rewrite their implementation, because a default implementation is provided.

Here is an example of how you will declare a default implementation in an interface:

public interface Collection<T> extends Iterable<T> {

    <R> Collection<R> filter(Predicate<T> p)
        default { return Collections.<T>filter(this, p); }

}

什么是虚拟扩展方法?

Java 8 将引入虚拟扩展方法的概念,也叫 public defender method. 让我们姑且把这个概念简化为 VEM。

VEM 旨在为 Java 接口提供默认的方法定义,你可以用它在已有的接口中添加新的方法定义,例如 Java 里的集合 API。这样类似 Hibernate 这样的第三方库无需重复实现这些集合 API 的所有方法,因为已经提供了一些默认方法。

下面是如何在接口中定义方法的示例:

public interface Collection<T> extends Iterable<T> {

    <R> Collection<R> filter(Predicate<T> p)
        default { return Collections.<T>filter(this, p); }

}

naive emulation of mixin in Java 8

Now, we will implement a mixin by using the VEMs. But I have to warn you first: Please, don’t use it at work! Seriously, DON’T USE IT AT WORK!

The implementation below is not thread-safe, subject to memory leak, and completely depends on the way you have defined hashCode and equals in your classes. There is another major drawback that I will discuss later.

First, we define an interface with a default implementation that seems to be based on a stateful approach.

public interface SwitchableMixin {

    boolean isActivated() default { return Switchables.isActivated(this); }

    void setActivated(boolean activated) default { Switchables.setActivated(this, activated); }

}

Second, we define a utility class that owns a map to store associations between the instances of our previous interface and their state. The state is represented by a private nested class in the utility class.

public final class Switchables {

    private static final Map<SwitchableMixin, SwitchableDeviceState> SWITCH_STATES = new HashMap<>();

    public static boolean isActivated(SwitchableMixin device) {
        SwitchableDeviceState state = SWITCH_STATES.get(device);
        return state != null && state.activated;
    }

    public static void setActivated(SwitchableMixin device, boolean activated) {
        SwitchableDeviceState state = SWITCH_STATES.get(device);
        if (state == null) {
            state = new SwitchableDeviceState();
            SWITCH_STATES.put(device, state);
        }
        state.activated = activated;
    }

    private static class SwitchableDeviceState {
        private boolean activated;
    }

}

Here is a use case for the implementation above. It highlights the inheritance of the behavior and of the state.

private static class Device {}

private static class DeviceA extends Device implements SwitchableMixin {}

private static class DeviceB extends Device implements SwitchableMixin {}
DeviceA a = new DeviceA();
DeviceB b = new DeviceB();

a.setActivated(true);

assertThat(a.isActivated()).isTrue();
assertThat(b.isActivated()).isFalse();

Java 8 对混入的模拟

现在我们来通过 VEM 实现一个混入效果,不过事先警告的是:请不要在工作中使用!

下面的实现不是线程安全的,而且还可能存在内存泄露问题,这取决于你在类中定义的 hashCode 和 equals 方法,这也是另外一个缺点,我将在后面讨论这个问题。

首先我们定义一个接口(模拟状态Bean)并提供方法的默认定义:

public interface SwitchableMixin {
    boolean isActivated() default { return Switchables.isActivated(this); }
    void setActivated(boolean activated) default { Switchables.setActivated(this, activated); }
}

然后我们定义一个工具类,包含一个 Map 实例来保存实例和状态的关联,状态通过工具类中的私有的嵌套类代表:

public final class Switchables {

    private static final Map<SwitchableMixin, SwitchableDeviceState> SWITCH_STATES = new HashMap<>();

    public static boolean isActivated(SwitchableMixin device) {
        SwitchableDeviceState state = SWITCH_STATES.get(device);
        return state != null && state.activated;
    }

    public static void setActivated(SwitchableMixin device, boolean activated) {
        SwitchableDeviceState state = SWITCH_STATES.get(device);
        if (state == null) {
            state = new SwitchableDeviceState();
            SWITCH_STATES.put(device, state);
        }
        state.activated = activated;
    }

    private static class SwitchableDeviceState {
        private boolean activated;
    }

}

这里是一个使用用例,突出了状态的继承:

private static class Device {}

private static class DeviceA extends Device implements SwitchableMixin {}

private static class DeviceB extends Device implements SwitchableMixin {}

“And Now for Something Completely Different”
This implementation seems to work well. But Brian Goetz, the Oracle’s Java Language Architect, has suggested me a puzzle where the current implementation does not work (assuming thread-safety and memory leak are fixed).

interface FakeBrokenMixin {
    static Map<FakeBrokenMixin, String> backingMap
        = Collections.synchronizedMap(new WeakHashMap<FakeBrokenMixin, String>());

    String getName() default { return backingMap.get(this); }
    void setName(String name) default { backingMap.put(this, name); }
}

interface X extends Runnable, FakeBrokenMixin {}

X makeX() { return () -> { System.out.println("X"); }; }

    X x1 = makeX();
    X x2 = makeX();
    x1.setName("x1");
    x2.setName("x2");

    System.out.println(x1.getName());
    System.out.println(x2.getName());

Can you guess what this should display?

Solution to the puzzle

At first sight, there seems to be no problem with this implementation. There, X is an interface with a single method, because the method getName and setName have a default implementation and not the method run fromRunnable. So we can generate an instance of X from a lambda expression, that will provide an implementation to the method run, as the method makeX does. So, you expect that this program displays something like:

x1
x2

If you remove the calls to getName
, you expect to display something like:

MyTest$1@30ae8764
MyTest$1@123acf34

Where the two lines indicates that makeX produces two separate instances. And this what the current OpenJDK 8 produces (here I have used the OpenJDK 8 24.0-b07).

However, the current OpenJDK 8 does not reflect the eventual behavior of Java 8. To do so, you need to runjavac with the special option -XDlambdaToMethod. Here is the kind of output you get once the special option is used:

x2
x2

If you remove the calls to getName
, you will get something like:

MyTest$$Lambda$1@5506d4ea
MyTest$$Lambda$1@5506d4ea

Each call to makeX seems to give a singleton instantiated from the same anonymous inner class. Nevertheless, if you look at the directory holding the Java binaries (having carefully removed all *.class files before), you will not find a file named MyTestClass$$Lambda$1.class.

The complete translation of the lambda expression is not really done at compile time. In fact, this translation is done at compile time and runtime. The compiler javac put in place of lambda expression an instruction recently added to JVM : the instruction invokedynamic (JSR292). This instruction comes with all the necessary meta-information to the translation of the lambda expression at runtime. This includes the name of the method to call, its input and output types, and also a method called bootstrap. The bootstrap method aims to define the instance that receive the method call, once the JVM execute the instruction invokedynamic. In presence of lambda expression the JVM uses a particular bootstrap called lambda metafactory method.

To come back to the puzzle, the body of the lambda expression is converted into a private static method. Thus, () -> { System.out.println("X"); } is converted in MyTest into

private static void lambda$0() {
    System.out.println("X");
}

his can be seen if you use the decompiler javap with the option -private (you can even use the -c option to see the more complete translation).

When you run the program, once the JVM tries to interpret the invokedynamic instruction for the first time, the JVM call the lambda metafactory method, describes above. In our example, during the first call to makeX, the lambda metafactory method generates an instance of X and links the method run (from the interface Runnable) dynamically to the method lambda$0. The instance of X is then stored in the memory. During the second call tomakeX, the instance is brought back. There you get the same instance as during the first call.

Fix? Workaround?

There is no direct fix or workaround for this problem. Even if it is planned to activate -XDlambdaToMethod by default for Oracle’s Java 8, this is not supposed to appear in the JVM specification. This kind of behavior might change over the time and among vendors. For a lambda expression, what you can only expect is to get something that implements your interface.

“完全不同的东西”

上面的实现跑起来似乎挺正常的,但 Oracle 的 Java 语言架构师 Brian Goetz 向我提出一个疑问说当前实现是无法工作的(假设线程安全和内存泄露问题已解决)

interface FakeBrokenMixin {
    static Map<FakeBrokenMixin, String> backingMap
        = Collections.synchronizedMap(new WeakHashMap<FakeBrokenMixin, String>());

    String getName() default { return backingMap.get(this); }
    void setName(String name) default { backingMap.put(this, name); }
}

interface X extends Runnable, FakeBrokenMixin {}

X makeX() { return () -> { System.out.println("X"); }; }

    X x1 = makeX();
    X x2 = makeX();
    x1.setName("x1");
    x2.setName("x2");

    System.out.println(x1.getName());
    System.out.println(x2.getName());

你猜这段代码执行后会显示什么结果呢?

疑问的解决

第一眼看去,这个实现的代码没有问题。X 是一个只包含一个方法的接口,因为 getName 和 setName 已经有了默认的定义,但 Runable 接口的 run 方法没有定义,因此我们可通过 lambda 表达式来生成 X 的实例,然后提供 run 方法的实现,就像 makeX 那样。因此,你希望这个程序执行后显示的结果是:

x1
x2

如果你删掉 getName 方法的调用,那么执行结果变成:

MyTest$1@30ae8764
MyTest$1@123acf34

这两行显示出 makeX 方法的执行来自两个不同的实例,而这时当前 OpenJDK 8 生成的(这里我使用的是 OpenJDK 8 24.0-b07).

不管怎样,当前的 OpenJDK 8 并不能反映最终的 Java 8 的行为,为了解决这个问题,你需要使用特殊参数 -XDlambdaToMethod 来运行 javac 命令,在使用了这个参数后,运行结果变成:

x2
x2

如果不调用 getName 方法,则显示:

MyTest$$Lambda$1@5506d4ea
MyTest$$Lambda$1@5506d4ea

每个调用 makeX 方法似乎都是来自相同匿名内部类的一个单例实例,如果观察包含编译后的 java class 文件的目录,会发现并没有一个名为 MyTestClass$$Lambda$1.class 的文件。

因为在编译时,lambda 表达式并没有经过完整的翻译,事实上这个翻译过程是在编译和运行时完成的,javac 编译器将 lambda 表达式变成 JVM 新增的指令 invokedynamic (JSR292)。这个指令包含所有必须的关于在运行时执行 lambda 表达式的元信息。包括要调用的方法名、输入输出类型以及一个名为 bootstrap 的方法。bootstrap 方法用于定义接收此方法调用的实例,一旦 JVM 执行了 invokedynamic 指令,JVM 就会在特定的 bootstrap 上调用 lambda 元工厂方法 (lambda metafactory method)。 

再回到刚才那个疑问中,lambda 表达式转成了一个私有的静态方法,() -> { System.out.println("X"); } 被转到了 MyTest:

private static void lambda$0() {
    System.out.println("X");
}

如果你用 javap 反编译器并使用 -private 参数就可以看到这个方法,你也可以使用 -c 参数来查看更加完整的转换。

当你运行程序时,JVM 会调用 lambda metafactory method 来尝试阐释 invokedynamic 指令。在我们的例子中,首次调用 makeX 时,lambda metafactory method 生成一个 X 的实例并动态链接 run 方法到 lambda$0 方法. X 的实例接下来被存储在内存中,当第二次调用 makeX 时就直接从内存中读取这个实例,因此你第二次调用的实例跟第一次是一样的。

修复了吗?有解决办法吗?

目前尚无这个问题直接的修复或者是解决办法。尽管 Oracle 的 Java 8 计划默认激活-XDlambdaToMethod 参数,因为这个参数并不是 JVM 规范的一部分,因此不同供应商和 JVM 的实现是不同的。对一个 lambda 表达式而言,你唯一能期望的就是在类中实现你的接口方法。

Another approach

So our emulation of mixins is not compatible with Java 8. But there is still a possibility to add services to an existing class by using multiple inheritance and delegation. This approach is referenced as virtual field pattern.

So lets start again with our Switchable.

interface Switchable {    boolean isActive();
    void setActive(boolean active);
}

We need an interface based on Switchable
 with an additional abstract method that returns an implementation of Switchable
. The inherited methods get a default implementation: they use the previous getter to transmit the call to the Switchable
 implementation.

public interface SwitchableView extends Switchable {
    Switchable getSwitchable();


    boolean isActive() default { return getSwitchable().isActive(); }
    void setActive(boolean active) default { getSwitchable().setActive(active); }
}

Then, we create a complete implementation of Switchable
.

public class SwitchableImpl implements Switchable {


    private boolean active;


    @Override
    public boolean isActive() {
        return active;
    }


    @Override
    public void setActive(boolean active) {
        this.active = active;
    }
}

Here is an example where we use the virtual field pattern.

public class Device {}


public class DeviceA extends Device implements SwitchableView {
    private Switchable switchable = new SwitchableImpl();


    @Override
    public Switchable getSwitchable() {
        return switchable;
    }
}


public class DeviceB extends Device implements SwitchableView {
    private Switchable switchable = new SwitchableImpl();


    @Override
    public Switchable getSwitchable() {
        return switchable;
    }
}
DeviceA a = new DeviceA();DeviceB b = new DeviceB();

a.setActive(true);

assertThat(a.isActive()).isTrue();
assertThat(b.isActive()).isFalse();

其他的方法

到此为止,尽管我们对 mixin 的模仿并不能兼容 Java 8,但还是可能通过多继承和委派为已有的类添加多个服务。这个方法就是 virtual field pattern (虚拟字段模式).

所以来看看我们的 Switchable.

interface Switchable {    boolean isActive();
    void setActive(boolean active);
}

我们需要一个基于 Switchable 的接口,并提供一个附加的抽象方法返回 Switchable 的实现。集成的方法包含默认的定义,它们使用 getter 来转换到 Switchable 实现的调用:

public interface SwitchableView extends Switchable {
    Switchable getSwitchable();


    boolean isActive() default { return getSwitchable().isActive(); }
    void setActive(boolean active) default { getSwitchable().setActive(active); }
}

接下来,我们创建一个完整的 Switchable 实现:

public class SwitchableImpl implements Switchable {


    private boolean active;


    @Override
    public boolean isActive() {
        return active;
    }


    @Override
    public void setActive(boolean active) {
        this.active = active;
    }
}

这里是我们使用虚拟字段模式的例子:

public class Device {}


public class DeviceA extends Device implements SwitchableView {
    private Switchable switchable = new SwitchableImpl();


    @Override
    public Switchable getSwitchable() {
        return switchable;
    }
}


public class DeviceB extends Device implements SwitchableView {
    private Switchable switchable = new SwitchableImpl();


    @Override
    public Switchable getSwitchable() {
        return switchable;
    }
}

Conclusion

In this post, we have seen two approaches to add services to a class in Java 8 with the help of virtual extension methods. The first approach uses a Map to store instance states. This approach is hazardous for more than one reason: it can be not thread-safe, there is a risk of memory leaks, it depends on the way your vendor has implemented the language, etc. Another approach, based on delegation and referenced as virtual field pattern, uses an abstract getter and let the final implementation describes which implementation of a service it uses. This last approach is more independent from the way the language is implemented and more secure.

The virtual extension method is something new in Java. It brings another mean of expression to create new patterns and best pratices. This article provides an example of such a pattern, enabled in Java 8 due to the use of virtual extension methods. I am sure you can extract other new patterns from them. But, as I have experienced here, you should not hesitate to share them in a view to check their validaty.


EDIT: after sharing in the lambda project mailing list, some modifications has needed to be reported in this post, in a view to reflect the new behavior of Java 8. Thank to Brian Goetz from Oracle and Yuval Shavit from Akiban for their suggestions.

  • [2012-07-11] changed title + renamed “How to emulate mixin in Java 8?” to “Naive emulation of mixin in Java 8″ + added a puzzle
  • [2012-07-12] added a section + removed the conclusion
  • [2012-07-22] added the solution to the puzzle
  • [2012-07-24] added fix/workaround section + some correction
  • [2012-08-13] completed all sections

结论

在这篇文章中,我们使用了两种方法通过 Java 8 的虚拟扩展方法为类增加多个服务。第一个方法使用一个 Map 来存储实例状态,这个方法很危险,因为不是线程安全而且存在内存泄露问题,这完全依赖于不同的 JVM 对 Java 语言的实现。另外一个方法是使用虚拟字段模式,通过一个抽象的 getter 来返回最终的实现实例。第二种方法更加独立而且更加安全。

虚拟扩展方法是 Java 的新特性,本文主要介绍的是多重继承的实现,详细你会有更深入的研究以及应用于其他方面,别忘了跟大家分享。


via:oschina

转载请注明:AspxHtml学习分享网 » Java 8 Lambda 表达式:模拟 Mixin 实现类的多重继承(Java 8: Now You Have Mixins?)

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址