1

I never worked with type aliases, but this concept seems to be a very useful feature adding semantics over same-typed objects and defending from common typos.

Let's say, there is void foo(float volume, float weight). It's ok if it's invoked like this: foo(v, m), but foo(m, v) is not an obvious typo. void process(Iterable<File> javaPath, Iterable<File> classPath) can be another use case. Unfortunately there are no type aliases in Java, and some workarounds are a true overkill:

  • aggregating a single field into a class (boxing a primitive into an object; an object in an object having an extra reference; more complicated serializing/deserializing rules)
  • extending one class from another one (like how do i create some variable type alias in Java - impossible for primitives; classes may be final or have an inaccessible constructor).

So both of them have drawbacks and runtime/performance cost.

As far as I can see, in Java, runtime-based "type aliasing" might be replaced with compile-time checks, like @NotNull and @Nullable are processed. Are there any static type-alias checkers/APT tools featuring support for constructions like void foo(@Volume float volume, @Weight float weight), so that checker could verify such "type safety" at compile time and require passed variables/constants and literals to be annotated (at declaration and call sites respectively)?


This question also has been raised because I'm currently working on a Java-source code processing tool that can use multiple back-ends (currently, JDT only). As long as I want it to be JDT-agnostic, I would like to have business objects for types, methods and fields. Despite, I lose a lot of information coming from JDT AST, the best current approach for my tool is use of strings (it allows to perform some analysis that's enough for the scope of the tool). But methods like void process(String type, String field, String method) are confusing, so I've created Type, Field and Method that represent the domain objects and are JDT-agnostic. They're fine, but I don't expect them to be extended in the future (all of these three types have single private final String name; field + I had to override hashCode/equals and toString). That led me to the idea of type-checking APT once again. As long as it's ok to process raw data, it would be enough to have something like void process(@Type String type, @Field String field, @Method String method) (I still could use a single object).

Community
  • 1
  • 1
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • I don't get it. If I am calling `foo(a,b)` for which both a and b are `int`, how is the tools supposed to verify if my `a` is a `Volume`? If you need `Volume` to bear a semantic meaning, make it a class. – Adrian Shum Nov 27 '14 at 08:32
  • @AdrianShum I just need more semantics over a float. Wrapping a single primitive into a class leads to another class instantiation (more heap), requires more adapters in serializers/deserializers (for example, GSON doesn't work for the new class -- more adapters needed). The idea is just like the idea of NotNull and Nullable is -- you don't create classes just to add semantics, you annotate, right? .NET supports value types that can hold primitives easily without performance cost (it doesn't have primitives per se, everything is a true object). But there are no value types in Java. – Lyubomyr Shaydariv Nov 27 '14 at 08:41
  • Unless you can tell me how it can check an input `int` is a `Volume` or not, I don't think it is a feasible idea to use annotation in such case. If you are looking for way to reduce unexpected typo, a fluent-builder like pattern maybe what you want: something like `foo().withVolume(a).withWeight(b).run()`. Of course there are extra development effort but if the method parameter is really that ambiguous and easy to mix up, it worth the extra effort. And, value object in C# doesn't solve the problem. Yes, instantiating from stack is having smaller overhead, but – Adrian Shum Nov 27 '14 at 08:54
  • there are still extra overhead if my understand is right. And there is no straight-forward solution for this problem in Java. If the situation is really that bad, that little extra performance penalty for that wrapper class shouldn't be a big problem. There are lots of way to reduce extra overheads: flyweight, factory-methods, immutable obj etc. – Adrian Shum Nov 27 '14 at 08:57
  • @AdrianShum yes, the builder is an extra option too, but I prefer to pass the arguments separately in most cases: two separated modules can still be independent. Also, the result of the builder may grow in time and the call-site could still use just a few fields of the domain object. I really prefer passing separated arguments (especially, for interface methods), because the method signatures just can tell what's the real expected data. – Lyubomyr Shaydariv Nov 27 '14 at 08:58
  • 1
    hm.... I am a bit confused. Using fluent-builder-like pattern doesn't make your modules inter-dependent. It is simply another way of writing your method – Adrian Shum Nov 27 '14 at 08:59
  • @Adrian sorry for confusion. You meant that `foo().withVolume(a).withWeight(b).run()` returns a domain object (I'm confused here too, expected `build()` instead of `run()`)? – Lyubomyr Shaydariv Nov 27 '14 at 09:02
  • that's why I said it is fluent-builder-**like** pattern. If making each parameter clear for its intention is your aim, I may add an answer to demonstrate how to do it. – Adrian Shum Nov 27 '14 at 09:04
  • @AdrianShum yes, please. – Lyubomyr Shaydariv Nov 27 '14 at 09:06

1 Answers1

0

If your problem is it looks ambiguous when you call foo(v,m) that people may mistakenly wrote foo(m,v) without any warning or visible clue of error, you may use a fluent-builder-like pattern:

(Code not tested, just wrote to demonstrate the idea)

public class MyClass {
    public class FooStub {
        int volume;
        int weight;
        public FooStub withVolume(int v) {
            this.volume = v;
            return this;
        }
        public FooStub withWeight(int w) {
            this.weight = w;
            return this;
        }
        public int run() {   // or call it getResult() or whatever you want
            Assert.notNull(volume);
            Assert.notNull(weight);
            return foo(volume,weight);  // calling method of outer class
        }
    }

    public int foo(int volume, int weight) {   // you even hide this if you want
        return volume * weight;
    }
    public FooStub foo() {
        return new FooStub();
    }

}

By doing so, caller can do

MyClass myObj = ....;
int result = myObj.foo().withVolume(a).withWeight(b).run();
// which is the same as result = myObject.foo(a,b);
Adrian Shum
  • 38,812
  • 10
  • 83
  • 131
  • Now I see your point better, thanks. I guess here comes another thing: `myObj.foo().withVolume(b).withWeight(a).run()` -- here `a` and `b` are swapped accidentally. – Lyubomyr Shaydariv Nov 27 '14 at 09:19
  • with a proper naming convention, you can spot it by eye. With the plain old method, if you do `myObj.foo(w,v);`, it still "looks" good, while `myObj.foo().withVolume(w).withWeight(v).run()` is not – Adrian Shum Nov 27 '14 at 09:21
  • Yes, so this is why I want a tool to catch such cases and add more semantics. For example, IntelliJ IDEA has some internal inspections that try (because they may not be that reliable) to cover suspicious calls showing a warning. – Lyubomyr Shaydariv Nov 27 '14 at 09:26
  • As I said, if you need to give more semantic meaning, creating another class is almost the only way you can do in Java. Annotation can hardly help in such case. Just tell me, how are you going to decide if an integer passed in is a `@Volume`? – Adrian Shum Nov 27 '14 at 10:26
  • If you are thinking of annotating the local variable with same annotation, afaik, there is no way you can handle local variable annotation in apt or runtime. You need to either parse the source code yourself, or use ASM to dig into the byte code and check it in runtime. Interesting idea though :) would be great if you can do it and share to the community :) – Adrian Shum Nov 27 '14 at 10:32
  • Not sure exactly, but something like: `void foo(@Volume int v)`, and something like `@Volume final int vol = 2; final int age = 2;` where `foo(vol);` works fine, and `foo(age);` might throw a warning. – Lyubomyr Shaydariv Nov 27 '14 at 10:34
  • I don't need runtime checks. Compile-time only, like this one: https://code.google.com/p/immutablej/ – Lyubomyr Shaydariv Nov 27 '14 at 10:35
  • As I have already answered in my comment: you need to do the parsing by yourself. Current APT is incapable to let you gather such information (e.g. local variable). – Adrian Shum Nov 28 '14 at 06:23
  • As a proof of concept, please see https://github.com/lyubomyr-shaydariv/type-alias-apt . Just to test, please run `mvn install` from `type-alias-apt` and `mvn compile` from `type-alias-apt-test`. A sample report should be logged for https://github.com/lyubomyr-shaydariv/type-alias-apt/blob/master/type-alias-apt-test/src/main/java/typealias/Test.java : `weight -> [@Weight()]`, `volume -> [@Volume()]`, `mutableWeight -> [@Weight()]` and `mutableVolume -> [@Volume()]` . As far as I can see, this allows to perform some kind of analysis similar to immutablej mentioned above, @Nullable/@Nonnull, etc – Lyubomyr Shaydariv Dec 07 '14 at 21:43