简体   繁体   中英

Groovy @Immutable classes in Java

I often recommend Groovy's @Immutable AST transformation as an easy way to make classes, well, immutable. This always works fine with other Groovy classes, but someone recently asked me if I could mix those classes into Java code. I always thought the answer was yes, but I'm hitting a snag.

Say I have an immutable User class:

import groovy.transform.Immutable

@Immutable
class User {
    int id
    String name
}

If I test this using a JUnit test written in Groovy, everything works as expected:

import org.junit.Test

class UserGroovyTest {

    @Test
    void testMapConstructor() {
        assert new User(name: 'name', id: 3)
    }

    @Test
    void testTupleConstructor() {
        assert new User(3, 'name')
    }

    @Test
    void testDefaultConstructor() {
        assert new User()
    }

    @Test(expected = ReadOnlyPropertyException)
    void testImmutableName() {
        User u = new User(id: 3, name: 'name')
        u.name = 'other'
    }
}

I can do the same with a JUnit test written in Java:

import static org.junit.Assert.*;

import org.junit.Test;

public class UserJavaTest {

    @Test
    public void testDefaultCtor() {
        assertNotNull(new User());
    }

    @Test
    public void testTupleCtor() {
        assertNotNull(new User(3, "name"));
    }

    @Test
    public void testImmutableName() {
        User u = new User(3, "name");
        // u.setName("other") // Method not found; doesn't compile
    }
}

This works, though there are troubles on the horizon. IntelliJ 15 doesn't like the call to new User() , claiming that constructor is not found. That also means the IDE underlines the class in red, meaning it has a compilation error. The test passes anyway, which is a bit strange, but so be it.

If I try to use the User class in Java code directly, things start getting weird.

public class UserDemo {
    public static void main(String[] args) {
        User user = new User();
        System.out.println(user);
    }
}

Again IntelliJ isn't happy, but compiles and runs. The output is, of all things:

User(0)

That's odd, because although the @Immutable transform does generate a toString method, I rather expected the output to show both properties. Still, that could be because the name property is null, so it's not included in the output.

If I try to use the tuple constructor:

public class UserDemo {
    public static void main(String[] args) {
        User user = new User(3, "name");
        System.out.println(user);
    }
}

I get

User(0, name)

as the output, at least this time (sometimes it doesn't work at all).

Then I added a Gradle build file. If I put the Groovy classes under src\\main\\groovy and the Java classes under src\\main\\java (same for the tests but using the test folder instead), I immediately get a compilation issue:

> gradle test
error: cannot find symbol
User user = new User(...)
^

I usually fix cross-compilation issues like this by trying to use the Groovy compiler for everything. If I put both classes under src\\main\\java , nothing changes, which isn't a big surprise. But if I put both classes under src\\main\\groovy , then I get this during the compileGroovy phase:

> gradle clean test
error: constructor in class User cannot be applied to the given types;
User user = new User(3, "name");

required: no arguments
found: int,String
reason: actual and formal arguments differ in length

Huh. This time it's objecting to the tuple constructor, because it thinks it only has a default constructor. I know the transform adds a default, a map-based, and a tuple constructor, but maybe they're not being generated in time for the Java code to see them.

Incidentally, if I separate the Java and Groovy classes again, and add the following to my Gradle build:

sourceSets {
    main {
        java { srcDirs = []}
        groovy { srcDir 'src/main/java' }
    }
}

I get the same error. If I don't add the sourceSets block, I get the User class not found error from earlier.

So the bottom line is, what's the correct way to add an @Immutable Groovy class to an existing Java system? Is there some way to get the constructors to be generated in time for Java to see them?

I've been making Groovy presentations to Java developers for years and saying you can do this, only to now run into problems. Please help me save face somehow. :)

I did try your scenario as well, where you have a single project with a src/main/java and a src/main/groovy directory and ended up with compilation errors similar to what you saw.

I was able to use Groovy immutable objects in Java when I put the Groovy immutables in a separate project from the Java code. I have created a simple example and pushed it to GitHub ( https://github.com/cjstehno/immut ).

Basically it's a Gradle multi-project with all the Groovy code (the immutable object) in the immut-groovy sub-project and all the Java code in the immut-java project. The immut-java project depends on the immut-groovy project and uses the immutable Something object:

public class SomethingFactory {

    Something createSomething(int id, String label){
        return new Something(id, label);
    }
}

I added a unit test in the Java project which creates a new instance of the immutable Groovy class and verifies its contents.

public class SomethingFactoryTest {
    @Test
    public void createSomething(){
        Something something = new SomethingFactory().createSomething(42, "wonderful");

        assertEquals(something.getId(), 42);
        assertEquals(something.getLabel(), "wonderful");
    }
}

This is not really ideal, but it works.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM