9

I was wondering if there is a Java API that could tell you whether a particular language feature (e.g. "diamond" operator) is available on the current platform.

(In other words, what I'm trying to do is analogous to "browser sniffing" in JavaScript.)

This would be really handy in meta-programming (writing a Java program that generates Java source code.

The best solution I've found so far is to parse System.getProperty("java.specification.version") and check whether it's ≥ the version that introduced this feature, but I'm not 100% sure that this property is available in all JVMs (or even whether it conforms to the same syntax in all JVMs). Another minor annoyance with this approach is that you have to take the extra step of looking up which version of Java introduced the language feature you're interested in. Not a big deal, since that info is pretty easy to google, but ideally it would be nice if there was an API that could readily provide the info, for example:

code.append("Map<Integer, String> map = ");
if (javax.meta.JavaVersion.getCurrentVersion().supportsDiamond()) {
    code.append("new Map<>();");
} else {
    code.append("new Map<Integer, String>();");
}

Obviously there's no package named javax.meta, but I was wondering if there might already be an existing solution for this problem that's cleaner than parsing the "java.specification.version" property.


Update: I just realized that Package#getSpecificationVersion() also provides the same value as System.getProperty("java.specification.version") but is probably more reliable, because System properties are mutable. In other words, the best way to get the Java spec version is probably to call Package#getSpecificationVersion() on any "built-in" package. For example: String.class.getPackage().getSpecificationVersion()

typeracer
  • 759
  • 8
  • 11
  • 2
    I would just let the user choose the Java version of the generated Java program and avoid all of this annoying stuff... :) – Sweeper Jan 10 '19 at 10:17
  • 1
    The only other way I see is to programmatically compile a small program with the intended feature and see if compilation fails(`import java.util.*;class M{public static void main(String[]a){Map m = new HashMap<>();}}`), but it's a hack which will also make your code hardly maintainable and slower, so I'd just stick with deciding what to write depending on the installed compiler version. – BackSlash Jan 10 '19 at 10:19

3 Answers3

4

This will give you the version of java that the system is running.

 System.getProperty("java.version")

If you're running Java 9 and greater you can use:

Runtime.Version version = Runtime.version();

Java Docs

Just a note the Java versioning naming standard changed at Java 9 as well.

Java Version: 1.7, 1.8, 9, 10, 11

I don't have a solution for you to check for specific features.

Lance
  • 768
  • 7
  • 21
  • 1
    I appreciate your answer, but I'm already using the `java.specification.version` property (it returns something like `"1.8"`), which makes more sense in this scenario than `java.version` (which returns something like `"1.8.0_151"`), because I only need the language specification and `"1.8"` is easier to parse than `"1.8.0_151"`. However, thanks for the info about the new `Runtime.Version` class. Maybe some day I'll upgrade to Java 9 :) – typeracer Jan 10 '19 at 10:42
3

A feature could be: JDK 10

  1. Method : Optional.orElseThrow()
  2. API : API for Creating Unmodifiable Collections
  3. System Property : Fore example, to Disable JRE Last Usage Tracking
  4. GC enhancement (Full parallel)
  5. Javadoc Support : (For Multiple Stylesheets)

It Could also be a REMOVAL of feature : also in Java 10

  1. Removal of Support for Using Old LookAndFeel
  2. Removal of Runtime.getLocalizedInputStream and getLocalizedOutputStream Methods
  3. And so on..

So it is hard to check or discover programmatcally if a new feature exist or of it has been removed UNLESS you know what you are looking for, it needs to to be provided by Oracle itself as a documentation, feature name and description.

If we are going to create and API for that, we must get the list first from Oracle docs, and then do required checks for each feature to discover the current version or if it is supported.

Following is an example to programmaticaly check the compiler for a specific functionality.

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.Arrays;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject.Kind;

public class CompileSourceInMemory {
  public static void main(String args[]) throws IOException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();

    StringWriter writer = new StringWriter();
    PrintWriter out = new PrintWriter(writer);
    out.println("public class HelloWorld {");
    out.println("  public static void main(String args[]) {");
    out.println("    System.out.println(\"This is in another java file\");");    
    out.println("  }");
    out.println("}");
    out.close();
    JavaFileObject file = new JavaSourceFromString("HelloWorld", writer.toString());

    Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);
    CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits);

    boolean success = task.call();
    for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
      System.out.println(diagnostic.getCode());
      System.out.println(diagnostic.getKind());
      System.out.println(diagnostic.getPosition());
      System.out.println(diagnostic.getStartPosition());
      System.out.println(diagnostic.getEndPosition());
      System.out.println(diagnostic.getSource());
      System.out.println(diagnostic.getMessage(null));

    }
    System.out.println("Success: " + success);

    if (success) {
      try {
        Class.forName("HelloWorld").getDeclaredMethod("main", new Class[] { String[].class })
            .invoke(null, new Object[] { null });
      } catch (ClassNotFoundException e) {
        System.err.println("Class not found: " + e);
      } catch (NoSuchMethodException e) {
        System.err.println("No such method: " + e);
      } catch (IllegalAccessException e) {
        System.err.println("Illegal access: " + e);
      } catch (InvocationTargetException e) {
        System.err.println("Invocation target: " + e);
      }
    }
  }
}

class JavaSourceFromString extends SimpleJavaFileObject {
  final String code;

  JavaSourceFromString(String name, String code) {
    super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension),Kind.SOURCE);
    this.code = code;
  }

  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return code;
  }
}

See JDK 10 features

0xInfection
  • 2,676
  • 1
  • 19
  • 34
TiyebM
  • 2,684
  • 3
  • 40
  • 66
  • I like your suggestion re: using the compiler tool API. actually just started using https://github.com/OpenHFT/Java-Runtime-Compiler in my metaprogramming endeavors, but it didn't occur to me that it could also be used to help with feature discovery until you pointed that out. Thanks! +1 – typeracer Jan 11 '19 at 00:10
1

I just discovered another partial solution: javax.lang.model.SourceVersion. It still doesn't solve the problem entirely, but seems like a step in the right direction.

So instead of my old approach,

if (System.getProperty("java.specification.version").compareTo("1.7") >= 0)
   // do something that requires Java 7

you could write:

if (SourceVersion.latestSupported().ordinal() >= SourceVersion.RELEASE_7.ordinal()) 
    // do something that requires Java 7

(because SourceVersion has its enum constants declared in ascending order)

This class also provides the static methods isKeyword, isName, isIdentifier, which might be useful, but it was already reasonably easy to derive some of this info (e.g. Character.isJavaIdentifierPart(cp)).

Under the hood, SourceVersion also relies on reading the "java.specification.version" system property:

private static SourceVersion getLatestSupported() {
    try {
        String specVersion = System.getProperty("java.specification.version");

        if ("1.8".equals(specVersion))
            return RELEASE_8;
        else if("1.7".equals(specVersion))
            return RELEASE_7;
        else if("1.6".equals(specVersion))
            return RELEASE_6;
    } catch (SecurityException se) {}

    return RELEASE_5;
}

That's some smelly code right there! e.g. if someone calls System.setProperty("java.specification.version", "6.9"); then SourceVersion.latestSupported() will return RELEASE_5 ;)

I'm surprised that even Sun/Oracle didn't provide an "intrinsic" way to get the Java version directly from the VM (like the various intrinsic operations exposed by the sun.misc package)

typeracer
  • 759
  • 8
  • 11