Blog-Archiv

Donnerstag, 20. Januar 2022

Java String Concatenation Performance

Premature optimization is the root of all evil (Donald Knuth).
And it kills simplicity.


How to correctly implement Java string concatenations was a long discussed developer topic. What do you think is faster, this StringBuilder technique ...


    return new StringBuilder()
            .append("Hello World, current date and time is ")
            .append(new Date())
            .append(" and we are at time zone ")
            .append(TimeZone.getDefault())
            .toString();

... or this plus-operator technique:


    return
            "Hello World, current date and time is "+
            new Date()+
            " and we are at time zone "+
            TimeZone.getDefault();

?

Most people would say, the plus-operator was discouraged in the Java beginning years, thus the first snippet is faster. Wrong, they are equally fast. Here are some measurements over exactly these snippets, with warm-up and 1000000 repeats, results are in milliseconds:

Technique Test 1 Test 2 Test 3 Test 4
StringBuilder 917 939 1025 992
Plus-Operator 863 903 1045 996

But what about StringBuffer, is this faster?
No, it is not, it is a little slower than StringBuilder, but that depends.

Surprisingly in some cases the plus-operator is much faster than both StringBuilder and StringBuffer, but sometimes it is also ways slower. It depends on how the concatenation is done, in a loop, or in a programmed sequence.

Loop concatenation (STRING_APPEND_COUNT = 4):


    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < STRING_APPEND_COUNT; i++)
        stringBuilder.append(string);
    return stringBuilder.toString();
    


    String s = "";
    for (int i = 0; i < STRING_APPEND_COUNT; i++)
        s += string;
    return s;
    

Sequence concatenation:


    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(string);
    stringBuilder.append(string);
    stringBuilder.append(string);
    stringBuilder.append(string);
    return stringBuilder.toString();
    


    String s = ""+
        string+
        string+
        string+
        string;
    return s;
    

The used string in these examples is not final.
Here are results:

Technique Test 1 Test 2 Test 3 Test 4
StringBuilder Loop 490 379 449 432
Plus-Operator Loop 776 623 828 802
StringBuilder Sequence 319 337 420 348
Plus-Operator Sequence 228 260 265 262

StringBuilder is much faster in loops, but in sequences the plus-operator wins.

When I make string final, following can be seen:

Technique Test 1
StringBuilder Loop 321
Plus-Operator Loop 689
StringBuilder Sequence 210
Plus-Operator Sequence 90

A dramatic win of plus-operator in sequences. This is because the compiler optimizes such code chunks. Everything else must be done at runtime, where the StringBuilder wins.

If you want to hack more, here is the complete source of my experiment, it also includes StringBuffer (click to expand):
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public class StringConcatPerformanceTest
{
    public static void main(String[] args) {
        StringConcatPerformanceTest test = new StringConcatPerformanceTest();
        test.start(Usage.LOOP);
        test.start(Usage.SEQUENCE);
    }
    
    private enum Technique    // the string-concat technique to test
    {
        STRING_BUFFER,
        STRING_BUILDER,
        PLUS_OPERATOR,
    }

    private enum Usage    // the way how to use the class to test
    {
        SEQUENCE,
        LOOP,
    }

    private /*final*/ String string = "Hello";
    
    private long time1;
    private long time2;
    private long time3;
    
    public StringConcatPerformanceTest() {
        for (Technique technique : Technique.values())    // warm up
            for (Usage usage : Usage.values())
                run(technique, usage);
    }
    
    public void start(Usage usage)    {
        time1 = time2 = time3 = 0L;
        
        for (int i = 0; i < 1000000; i++)
            run(usage);
        
        System.out.println("StringBuffer  "+usage+": "+duration(time1));
        System.out.println("StringBuilder "+usage+": "+duration(time2));
        System.out.println("Plus Operator "+usage+": "+duration(time3));
        System.out.println("====================");
    }
    
    private String duration(long nanos)    {
        return ""+Math.round((double)nanos / 1000000)+" millis";
    }
    
    
    private void run(Usage usage)    {
        time1 += run(Technique.STRING_BUFFER, usage);
        time2 += run(Technique.STRING_BUILDER, usage);
        time3 += run(Technique.PLUS_OPERATOR, usage);
        
        time2 += run(Technique.STRING_BUILDER, usage);
        time3 += run(Technique.PLUS_OPERATOR, usage);
        time1 += run(Technique.STRING_BUFFER, usage);
        
        time3 += run(Technique.PLUS_OPERATOR, usage);
        time1 += run(Technique.STRING_BUFFER, usage);
        time2 += run(Technique.STRING_BUILDER, usage);
    }
    
    private long run(Technique technique, Usage usage)    {
        final long time = System.nanoTime();
        
        if (usage == Usage.SEQUENCE)    {
            switch (technique)    {
                case STRING_BUFFER: stringBufferSequence(); break;
                case STRING_BUILDER: stringBuilderSequence(); break;
                case PLUS_OPERATOR: plusOperatorSequence(); break;
                default: throw new IllegalArgumentException();
            }
        }
        else    {    // Usage.LOOP
            switch (technique)    {
                case STRING_BUFFER: stringBufferLoop(); break;
                case STRING_BUILDER: stringBuilderLoop(); break;
                case PLUS_OPERATOR: plusOperatorLoop(); break;
                default: throw new IllegalArgumentException();
            }
        }
        
        return System.nanoTime() - time;
    }
    
    
    private static final int STRING_APPEND_COUNT = 4;
    
    
    private String stringBufferLoop() {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            stringBuffer.append(string);
        return stringBuffer.toString();
    }

    private String stringBuilderLoop() {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            stringBuilder.append(string);
        return stringBuilder.toString();
    }

    private String plusOperatorLoop() {
        String s = "";
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            s += string;
        return s;
    }

    
    private String stringBufferSequence() {
        StringBuffer stringBuffer = new StringBuffer("");
        stringBuffer.append(string);    // 1
        stringBuffer.append(string);
        stringBuffer.append(string);
        stringBuffer.append(string);    // 4 == STRING_APPEND_COUNT
        return stringBuffer.toString();
    }

    private String stringBuilderSequence() {
        StringBuilder stringBuilder = new StringBuilder("");
        stringBuilder.append(string);    // 1
        stringBuilder.append(string);
        stringBuilder.append(string);
        stringBuilder.append(string);    // 4 == STRING_APPEND_COUNT
        return stringBuilder.toString();
    }

    private String plusOperatorSequence() {
        String s = "" +
            string +    // 1
            string +
            string +
            string ;    // 4 == STRING_APPEND_COUNT
        return s;
    }
}


I would always use the plus-operator by default. It is the much more readable and simpler variant. Just in cases where you really build big texts, and non-final fields or method calls are involved, you should use StringBuilder.




Samstag, 15. Januar 2022

How to Implement and Debug a Java Doclet

If you've never heard about Doclet, this is a JDK library that enables you to write your own variant of the javadoc tool that comes bundled with the JDK. This tool is for generating HTML pages from class-, field- and method- /** comments */ that were written in Java sources by developers.

Doclet has been reworked for modular Java, and its use was documented newly. Mind that some Java 8 documentation still refers to the old version (which says a lot about javadoc popularity). The best time of Doclet was before annotations were introduced with Java 1.6 in 2007, as it was used as replacement for annotations.

The interest in documentation never was great in developer circles, being called a "sweet code smell", and everybody was happy that he/she could believe in self-explanatory source code (which I regard to be a myth, but myths are more popular than reality). Nevertheless we all want autocomplete-tooltips in our IDE, showing us the documentation of the API that we need to call (because there is very few self-explanatory code in this world:-).

However, if you are interested in writing a Doclet, here is a description of how you can implement and launch it in your IDE's debugger. A Doclet normally is launched by -doclet and -docletpath options in command line of the javadoc tool, but it's not possible to debug it then, so we need a solution.

Overview

What we need to implement:

  1. A static Java launcher of the javadoc tool, with the options to use our doclet. This class can then be started in "Run" or "Debug" mode of your IDE.

  2. The Doclet interface. The resulting class can be used with the -doclet option in command line of the javadoc tool.

  3. The ElementVisitor interface that is accepted by the "elements" returned from DocletEnvironment.html#getSpecifiedElements() in the run() implementation of the Doclet interface.
    In this article you will find just an abstraction of that visitor (called ModelBuilder), because it is up to you to decide what your doclet should do.

Launcher and Doclet

I implemented a parameterizable launcher and the Doclet interface in just one class. This must be run from another class implementing a runnable main() method. You find an example at the end of this chapter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.tools.DocumentationTool;
import javax.tools.ToolProvider;

import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;

/**
 * A javadoc launcher that implements the Doclet interface.
 */
public class DocletLauncher implements Doclet
{
    /**
     * Launcher that calls javadoc.
     * @param docletClass the doclet class to use, value for "-doclet" option.
     * @param DOCLET_CLASSPATH the path where the doclet resides, value for "-docletpath".
     * @param SOURCE_BASEPATH the path where the sources reside, prefix for all sourceFiles.
     * @param sourceFiles Java sources to process.
     */
    public static void startDoclet(
            String DOCLET_CLASSPATH, 
            String SOURCE_BASEPATH,
            String[] sourceFiles)
    {
        // for every file, call javadoc
        for (String sourceFile : sourceFiles)    {
            if (sourceFile.endsWith(".java") == false)    {
                if (sourceFile.contains("."))
                    sourceFile = sourceFile.replace('.', '/');
                sourceFile = sourceFile+".java";
            }
                
            // use a custom stream to get file names and line numbers
            final String[] docletArguments = new String[] {
                    "-doclet", DocletLauncher.class.getName(), 
                    "-docletpath", DOCLET_CLASSPATH,
                    SOURCE_BASEPATH+"/"+sourceFile
                };
            final DocumentationTool javaDoc = ToolProvider.getSystemDocumentationTool();
            javaDoc.run(null, null, null, docletArguments);    // in, out, err
        }
    }
    
    /** Called by javadoc for source visitation. */
    @Override
    public boolean run(DocletEnvironment environment) {
        for (Element element : environment.getSpecifiedElements())    {
            final ModelBuilder modelBuilder = new ModelBuilder(environment.getDocTrees());
            element.accept(modelBuilder, null);
        }
        return true;
    }
    
    @Override
    public void init(Locale locale, Reporter reporter) {
    }
    
    @Override
    public String getName() {
        return getClass().getSimpleName();
    }

    @Override
    public Set<? extends Option> getSupportedOptions() {
        return Collections.emptySet();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }
}

The startDoclet() method on line 28 can be run from any class main() with parameters according to your development environment (IDE). For a simple Eclipse Java project (non-Maven), the DOCLET_CLASSPATH would be bin, for an Eclipse Maven module, target/classes. The SOURCE_BASEPATH must point to some path where the Java source files to analyze are below. Mind that you need to add all compile-dependencies to your CLASSPATH when scanning sources outside the module where your doclet resides, because javadoc uses the javac compiler!

On line 34, all source files given as arguments are looped and passed to the javadoc tool, one by one. On line 36, the arguments are converted into file system paths when given as fully-qualified class names.

Line 42 builds together the arguments to be passed to javadoc. Every source file is placed under the given SOURCE_BASEPATH.

Line 47 and 48 finally run the javadoc tool. This will trigger, for every source file in loop, the run() method on line 54. Mind that we skip from static usage to the Doclet instance here.

The run() method on line 55 loops all "specified elements" in given source file. Mind that if you have a public inner interface or class in your source file, that will be a separate "specified element", although it also will be contained in the main element.

The ModelBuilder on line 56 is not a JDK class, it is the ElementVisitor implementation specific to the doclet we provide here. You can see its abstraction below. The DocTrees object that is passed to it is a utility needed to retrieve comments, file paths and line numbers of the scanned source.

All other methods do not matter very much, they were not called by javadoc for a single Java file.


Example for a class that calls the launcher, which you would run in debug-mode from your IDE:

public class StartDoclet
{
    public static void main(String[] args) {
        final String DOCLET_CLASSPATH = "bin";    // or "target/classes"
        final String SOURCE_BASEPATH = "src";    // or "src/main/java"
        //final String SOURCE_BASEPATH = "../my_module/src/main/java/";
        
        DocletLauncher.startDoclet(
                DOCLET_CLASSPATH, 
                SOURCE_BASEPATH, 
                args);
    }
}

If you run StartDoclet from another module than where the source to scan is, you would pass ../my_module/src/main/java/ as SOURCE_BASEPATH, in case the source is in a parallel Maven-module called "my_module". It is important to also take that other project and all its dependencies into the CLASSPATH when running the StartDoclet, because javadoc uses the javac compiler, and that would fail when trying to compile some source without having its dependencies. For Eclipse, you can do that on the "Dependencies" tab in "Run/Debug Configuration".

Alternatively you can place a StartDoclet class in every module where you intend to scan source. In that case, each of these modules must refer to the DocletLauncher module, thus you would revert the dependency.

Element Visitor

Writing the Doclet was easy, here comes the hard part. It took me some time to find out all the details of the doclet API, for example it was really hard to get the source's file path, or the line number of some element.

The class below is an abstraction of what you need to implement for Java source visitation. It translates the quite complex doclet visitation to a simpler, supporting inner class nesting. All public visitXXX methods belong to the Doclet interface, all protected abstract visitXXX methods need to be overridden by a concrete ModelBuilder that would produce HTML code. Do not confuse these two method groups.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;

import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.DocTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.util.DocSourcePositions;
import com.sun.source.util.DocTrees;
import com.sun.source.util.TreePath;

/**
 * To be overridden for interpretation of one Java source file.
 */
public abstract class AbstractModelBuilder implements ElementVisitor<Void,Object>
{
    protected final DocTrees docTrees;
    private final Set<Integer> lineNumbers = new HashSet<>();

    public AbstractModelBuilder(DocTrees docTrees)    {
        this.docTrees = docTrees;
    }
    
    // START interface ElementVisitor
    
    @Override
    public final Void visitType(TypeElement element, Object parameter) {
        if (lineNumbers.isEmpty())
            if (visitFile(filePath(element)) == false)
            	return null;
        
        final int line = fileLine(element);
        lineNumbers.add(line);    // avoids receiving no-arg constructor created by compiler
        
        final String className = getName(element);
        visitClass(className, line, getComment(element), element);
        
        // to achieve nesting of inner classes, explicitly loop enclosed elements
        for (Element enclosed : element.getEnclosedElements())
            enclosed.accept(this, parameter);
        
        visitEndOfClass(className, element);    // terminate current inner class
        
        return null;
    }

    @Override
    public final Void visitVariable(VariableElement element, Object parameter) {
        visitFieldOrMethod(element, false);
        return null;
    }

    @Override
    public final Void visitExecutable(ExecutableElement element, Object parameter) {
        visitFieldOrMethod(element, true);
        return null;
    }
    
    @Override
    public final Void visit(Element e, Object p) {
        return null;
    }
    @Override
    public final Void visitPackage(PackageElement e, Object p) {
        return null;
    }
    @Override
    public final Void visitTypeParameter(TypeParameterElement e, Object p) {
        return null;
    }
    @Override
    public final Void visitUnknown(Element e, Object p) {
        return null;
    }
    
    // END interface ElementVisitor
    
    // START to be implemented by sub-classes
    
    /**
     * Sub-classes can process given source file path. By default this returns true.
     * @return true when processing of this file should continue, false to ignore it.
     */
    protected boolean visitFile(String filePath)	{
    	return true;
    }

    /** Sub-classes must process given top-level or inner class. */
    protected abstract void visitClass(String name, int line, String comment, TypeElement e);

    /** Sub-classes must process given field. */
    protected abstract void visitField(String name, int line, String comment, VariableElement e);

    /** Sub-classes must process given method. */
    protected abstract void visitMethod(String name, int line, String comment, ExecutableElement e);

    /** The current class ends now. */
    protected abstract void visitEndOfClass(String name, TypeElement e);

    // END to be implemented by sub-classes
    
    
    private final String filePath(TypeElement e)    {
        final CompilationUnitTree compilationUnit = docTrees.getPath(e).getCompilationUnit();
        final JavaFileObject fileObject = compilationUnit.getSourceFile();
        return fileObject.getName();
    }
    
    private void visitFieldOrMethod(Element e, boolean isMethod) {
        final int line = fileLine(e);
        // avoid multiple visitations due to explicitly looping enclosed elements
        if (lineNumbers.contains(line) == false)    {
            lineNumbers.add(line);
            if (isMethod)
                visitMethod(getName(e), line, getComment(e), (ExecutableElement) e);
            else
                visitField(getName(e), line, getComment(e), (VariableElement) e);
        }
    }
    
    private String getName(Element e) {
        return (e instanceof TypeElement) ? e.toString() : e.getSimpleName().toString();
    }

    private String getComment(Element e) {
        final DocCommentTree docCommentTree = docTrees.getDocCommentTree(e);
        if (docCommentTree == null)
            return null;
        
        final StringBuilder sb = new StringBuilder();
        final List<? extends DocTree> fullBody = docCommentTree.getFullBody();
        for (final DocTree commentFragment : fullBody)    {
            final String text = commentFragment.toString().trim();
            if (sb.length() <= 0)
                sb.append(text);
            else
                sb.append(" "+text);
        }
        final String result = sb.toString();
        return result.isBlank() ? null : result;
    }

    private final int fileLine(Element e)    {
        final TreePath path = docTrees.getPath(e);
        final CompilationUnitTree compilationUnit = docTrees.getPath(e).getCompilationUnit();
        final LineMap lineMap = compilationUnit.getLineMap();
        final DocSourcePositions spos = docTrees.getSourcePositions();
        final long startPos = spos.getStartPosition(compilationUnit, path.getLeaf());
        return (int) lineMap.getLineNumber(startPos);
    }
}

Besides the java.lang.model imports we also have com.sun.source now (don't ask me if this is a "legal" import), needed for DocTrees helpers.

Implementing ElementVisitor<Void,Object> on line 25 means that the visit-methods won't return anything (Void), and their second parameter will be of type Object (actually this is not used here, it would be the second parameter you pass to accept() in DocletLauncher). The constructor on line 30 stores the DocTrees utility, coming from the caller's DocletEnvironment.

Line 37 - 84 contain all implementations of the doclet ElementVisitor interface. Only three of them do something, because AbstractModelBuilder just catches classes, (visitType(), also all inner classes go here), fields (visitVariable()) and methods (visitExecutable()). The first visitType() call also exposes the source file path.

The visitType() implementation explicitly iterates over the enclosed elements on line 49. This technique requires to avoid duplicate calls, because all elements would be visited anyway also without this explicit loop. By storing line numbers we avoid duplicate calls, and achieve nesting of inner classes. The visitEndOfClass() call on line 52 notifies about exiting an inner class.

Both visitVariable() and visitExecutable() also need to comply with the line number management. To do this once-and-only-once, both call the same procedure on line 119, with a flag.

All other methods from line 70 - 84 are not called when scanning a single Java source file. Line 94 - 108 contain the methods you need to implement when you want to use this class for your doclet. Maybe you should add data types, and a list of parameters to method visiting. You can retrieve such informations from the Element derivations VariableElement and ExecutableElement.

On line 113 the internals start. It looks like we need to know about "compilation unit" to get the source file path on line 114, and the line numbers on line 153. Both make use of the DocTrees utlitity that was given to constructor.

Line 132 shows how to get the name of a class, field or method. All can be done with "simple name". The toString() method should not carry application semantic, but here it was done so. (Not the only inconsistency, why was field called "variable", or method "executable"?)

For getting the comment of some element on line 135 you would need a dedicated DocTreeVisitor that you can pass to every single DocTree item in list. I tried to make it simpler, again with the infamous toString() call. Complex comments may not look good, and @tags are not contained.

Resume

I hope this helps in case you need to write a doclet. The doclet API was made for experts, not for the masses. For me it was a frustrating experience, because I just wanted to achieve some source folding as it can be seen in my recent articles, and it took me days to come to an end. Let's hope that javadoc will become more popular again in the future, we need it!




Freitag, 14. Januar 2022

Highlighting Code as HTML by Internet API with Java

Highlighting source code as HTML is not so easy when you try to download some Java library and need to find out how to launch it on your code. But it's quite easy to copy & paste on the free website http://hilite.me. It supports a lot of programming languages, and it also offers a REST-API at http://hilite.me/api.

Here is a Java class that can call the API for you. It also shows how you can call a website by importing just JDK classes from java.net and java.io, not even using the new HttpClient. Mind that GET fails when the sent source code is too big, so I implemented a POST call, hilite.me accepts both. For a POST call, you must URL-encode the source code, and then write it together with all other parameters into the output-stream of HttpURLConnection, like done on line 49. You can see parameter names supported by hilite.me in the help text at the API location. The divstyles parameter doesn't work.

HiLiteJava.java
Highlights source code as HTML by calling http://hilite.me/api . Works only with valid Internet connection! Imports classes from java.net and java.io.
Field URL
private static final String URL = "http://hilite.me/api";
Field NEWLINE
private static final String NEWLINE = System.lineSeparator();
Field ENCODING
private static final String ENCODING = StandardCharsets.UTF_8.name();
Field parameters
private final String parameters;
Constructor activating line number generation.
public HiLiteJava() {
    this(true);
}
Constructor setting rendering style to "monokai".
public HiLiteJava(boolean lineNumbers) {
    this(lineNumbers, "monokai");
}
Constructor setting lexer to "java", see http://pygments.org/docs/styles/.
public HiLiteJava(boolean lineNumbers, String style) {
    this(lineNumbers, style, "java");
}
Constructor preparing all given parameters as URL parameters.
public HiLiteJava(boolean lineNumbers, String style, String lexer) {
    this.parameters = 
            "lexer="+lexer+"&"+
            "style="+style+"&"+
            (lineNumbers ? "linenos=1&" : "");
    // good styles are: "monokai", "borland", "emacs", "default", "manni", "trac" "vs"
}
Call the highlighting website with given source code and return the highlighted HTML representation. Mind that no line-formatting is done, so the given javaSourceCode should contain newlines and be indented correctly!
public String hilite(String javaSourceCode)    {
    try    {
        final URL url = new URL(URL);
        
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty("Accept-Charset", ENCODING);
        connection.setDoOutput(true); // triggers POST
        // big javaSourceCode works with POST only!
        try (final OutputStream output = connection.getOutputStream()) {
            String parameters = this.parameters+"code="+URLEncoder.encode(javaSourceCode, ENCODING);
            output.write(parameters.getBytes(ENCODING));
        }

        final int responseCode = connection.getResponseCode();
        if (responseCode != 200)
            throw new IllegalStateException("Response code was: "+responseCode);
        
        try (final BufferedReader inputReader = 
                    new BufferedReader(new InputStreamReader(connection.getInputStream(), ENCODING))) {
            final StringBuilder response = new StringBuilder();
            for (String line; (line = inputReader.readLine()) != null; ) {
                response.append(line);
                response.append(NEWLINE);
            }
            return response.toString();
        }
    }
    catch (Exception e)    {
        throw new RuntimeException(e);
    }
}
Test main(): try it out with a very short class!
public static void main(String[] args) throws Exception    {
    String javaSource = "class Foo\n{\n  void bar()\n  {\n  }\n}";
    System.out.println(new HiLiteJava().hilite(javaSource));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class HiLiteJava
{
    private static final String URL = "http://hilite.me/api";
    private static final String NEWLINE = System.lineSeparator();
    private static final String ENCODING = StandardCharsets.UTF_8.name();
    
    private final String parameters;
    
    /** Constructor activating line number generation. */
    public HiLiteJava() {
        this(true);
    }
    
    /** Constructor setting rendering style to "monokai". */
    public HiLiteJava(boolean lineNumbers) {
        this(lineNumbers, "monokai");
    }
    
    /** Constructor setting lexer to "java", see http://pygments.org/docs/styles/. */
    public HiLiteJava(boolean lineNumbers, String style) {
        this(lineNumbers, style, "java");
    }
    
    /** Constructor preparing all given parameters as URL parameters. */
    public HiLiteJava(boolean lineNumbers, String style, String lexer) {
        this.parameters = 
                "lexer="+lexer+"&"+
                "style="+style+"&"+
                (lineNumbers ? "linenos=1&" : "");
        // good styles are: "monokai", "borland", "emacs", "default", "manni", "trac" "vs"
    }
    
    /**
     * Call the highlighting website with given source code and
     * return the highlighted HTML representation.
     * Mind that no line-formatting is done, so the given javaSourceCode
     * should contain newlines and be indented correctly!
     * @param javaSourceCode the text to highlight as HTML.
     * @return the highlighted HTML representing given source.
     */
    public String hilite(String javaSourceCode)    {
        try    {
            final URL url = new URL(URL);
            
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestProperty("Accept-Charset", ENCODING);
            connection.setDoOutput(true); // triggers POST
            // big javaSourceCode works with POST only!
            try (final OutputStream output = connection.getOutputStream()) {
                String parameters = this.parameters+"code="+URLEncoder.encode(javaSourceCode, ENCODING);
                output.write(parameters.getBytes(ENCODING));
            }
    
            final int responseCode = connection.getResponseCode();
            if (responseCode != 200)
                throw new IllegalStateException("Response code was: "+responseCode);
            
            try (final BufferedReader inputReader = 
                        new BufferedReader(new InputStreamReader(connection.getInputStream(), ENCODING))) {
                final StringBuilder response = new StringBuilder();
                for (String line; (line = inputReader.readLine()) != null; ) {
                    response.append(line);
                    response.append(NEWLINE);
                }
                return response.toString();
            }
        }
        catch (Exception e)    {
            throw new RuntimeException(e);
        }
    }
    
    
    /** Test main(): try it out with a very short class! */
    public static void main(String[] args) throws Exception    {
        String javaSource = "class Foo\n{\n  void bar()\n  {\n  }\n}";
        System.out.println(new HiLiteJava().hilite(javaSource));
    }
}

All source code in this article has been highlighted by hilite.me. The style parameter I used was borland.

A big "Thank You" to the hilite.me website for supporting us freely! I hope it won't be closed ever. Also thanks for providing automation support, because I couldn't find any other site that offers such an API.