Java Constructor Reference: Syntax and Use
java constructor reference: Learn how to Java constructor references work, how to use them with functional interfaces, and where they fit in modern Java code.
A Java constructor reference is a compact form of a lambda expression that creates an instance of a class. It uses the syntax ClassName::new and can be passed wherever a functional interface that matches the constructor's signature is expected. For example, Supplier<ArrayList<String>> can be assigned ArrayList::new instead of writing () -> new ArrayList<>(). This article explains the syntax, the functional interfaces that make it work, practical examples, and the edge cases that often trip up developers.
n
The Constructor Reference Syntax
The core syntax for a constructor reference is ClassName::new. The compiler infers which constructor to use based on the target functional interface's method signature. For a no-argument constructor, the functional interface must have a method that takes no arguments and returns the class type. Supplier<T> fits this pattern exactly:
Supplier<LocalDate>> dateSupplier = LocalDate::new;
This is equivalent to the lambda () -> new LocalDate(). The constructor reference is not a special runtime object; it is a way to write the lambda more concisely. The compiler treats it as a method reference to the constructor, and the bytecode is generated via inveddynamic just like a lambda.
The same syntax works for constructors with parameters, as long as the functional interface's abstract method accepts the same argument types. For instance, Function<String, Integer> expects a method that takes a String and returns an Integer. The Integer class has a constructor that takes a String, so Integer::new is valid:
Function<String, Integer> parser = Integer::new; Integer value = parser.apply("42");
This is equivalent to s -> new Integer(s). Note that in modern Java, Integer.valueOf is usually preferred over the constructor, but the constructor reference still demonstrates the mechanism.
How Constructor References Work with Functional Interfaces
The key to understanding constructor references is that they are only valid when the target functional interface's method signature matches a constructor of the referenced class. The compiler performs the match at compile time. If no constructor matches, the code will not compile.
The functional interface can be a standard one from java.util.function or a custom interface. For example, consider a Person class with a constructor that takes a name and an age:
class Person { private final String name; private final int age; Person(String name, int age) { this.name = name; this.age = age; } }
n
To create a Person from two arguments, you need a functional interface with a two-argument method. BiFunction<String, Integer, Person> works because its apply method takes two arguments and returns a result:
BiFunction<String, Integer, Person> personFactory = Person::new; Person p = personFactory.apply("Alice", 30);
If you need more than two parameters, you must define your own functional interface because the standard library only provides Function, BiFunction, and Supplier for up to two arguments. This is a common reason to create a custom functional interface.
Practical Examples with Supplier and Function
The most common use of constructor references is with Supplier and Function. A Supplier<T> is used when you need a factory that returns a new instance each time. For example, in a stream pipeline, you might need a fresh collection to collect results:
List<String> names = Stream.of("Alice", "Bob") .collect(Collectors.toCollection(ArrayList::new));
Here ArrayList::new is a constructor reference that matches the Supplier<C> expected by toCollection. It creates a new ArrayList for each collection operation.
A Function<T, R> is useful when you need to transform an input into a new object. For example, parsing a a string to a BigInteger:
Function<String, BigInteger> toBigInteger = BigInteger::new; nBigInteger result = toBigInteger.apply("12345678901234567890");
The constructor reference works because BigInteger has a constructor that accepts a String. This pattern is common when mapping data from one representation to another.
Constructor References for Arrays and Collections
Constructor references are not limited to regular classes; they can also reference array constructors. The syntax uses the array type followed by ::new, for example int[]::new. This is useful when you need to create an array of a specific length at runtime. The functional interface IntFunction<T> has a method that takes an int and returns T, which matches array creation:
IntFunction<int[]> arrayCreator = int[]::new; int[] values = arrayCreator.apply(5); // creates int[5]
This is is particularly handy when working with streams and you need to convert a stream to an array. The toArray method on a stream expects an IntFunction<A[]>, so you can write:
String[] names = Stream.of("Alice", "Bob").toArray(String[]::new);
The compiler generates code that allocates a new array of the given size. This avoids writing a lambda like size -> new String[size].
For collections, constructor references are often used with Collectors.toCollection as shown earlier. The you can also use them to create a new collection from a stream of elements:
Set<String> uniqueNames = Stream.of("Alice", "Bob", "Alice") .collect(Collectors.toCollection(HashSet::new));
This ensures a HashSet is created rather than a default collection type.
Constructor References vs. Lambda Expressions
A constructor reference is essentially a shorthand for a lambda that calls the constructor. The two are interchangeable in most cases. The main difference is readability: ClassName::new is more concise and signals the intent to create a new instance. A lambda like () -> new ClassName() is more explicit but longer.
There is no functional difference in runtime behavior. Both compile to the same invokedynamic instruction and produce an instance of the functional interface. The constructor reference does not create an extra object or add overhead beyond what the lambda would. The choice between them is stylistic and about clarity.
However, there are cases where a lambda is necessary. If the constructor call requires additional logic, such as argument transformation or validation, you cannot use a constructor reference. For example, if you need to trim a string before passing it to the constructor, you must write a lambda:
Function<String, Person> personFactory = name -> new Person(name.trim(), 0);
A constructor reference would not allow you to modify the argument. Similarly, if you need to call a static factory method instead of a constructor, you use a method reference to that method, not a constructor reference.
Common Mistakes and Edge Cases
One common mistake is using a constructor reference when the functional interface expects a different signature. For instance, Function<Integer, String> expects a method that takes an Integer and returns a String. The String class does not have a constructor that takes an Integer, so String::new would not compile. The compiler error message is often confusing because it lists all constructors that do not match.
Another edge case involves generic classes. If you have a generic class Box<T>, you cannot write Box::new directly because the constructor's type parameter is not known at the reference site. Instead, you need to use a lambda or provide a type witness. For example:
Supplier<Box<String>> boxSupplier = () -> new Box<>();
This is a limitation of constructor references: they work best with concrete types. For generic types, you often have to fall back to a lambda.
A third edge case is with inner classes. Non-static inner classes have an implicit reference to the enclosing instance, so their constructor reference requires an instance of the outer class. The syntax becomes outerInstance::new rather than InnerClass::new. For example:
class Outer { class Inner {} n Supplier<Inner> innerSupplier = this::new; // not Inner::new }
This is subtle and can cause confusion. Static nested classes do not have this issue and can be referenced as NestedClass::new.
Runtime Overhead and When It Matters
Constructor references, like lambdas, are implemented using invokedynamic. The first time a constructor reference is used, the JVM resolves the call site and generates a class that implements the target functional interface. Subsequent uses reuse that generated class, so there is no repeated allocation of a new class. The overhead of creating the functional interface instance is minimal and comparable to that of a lambda.
In performance-sensitive code, the main cost is the actual object construction, not the reference itself. The constructor reference adds no extra allocation beyond the new object. However, if you are creating many objects in a hot loop, the cost of the constructor itself dominates. Using a constructor reference instead of a lambda does not change that cost.
A more important consideration is maintainability. Constructor references make the code more declarative and reduce boilerplate, which can improve readability. But they can hide what constructor is being called if the class has multiple constructors. In such cases, a lambda with an explicit new call may be clearer. Choose the form that best communicates intent to the next developer.
Compatibility with Java Versions
Constructor references were introduced in Java 8 and are supported in all later versions. They are part of the method reference syntax, which also includes static method references and instance method references. If you are working with code that targets Java 7 or earlier, you cannot use constructor references; you must use anonymous classes or lambdas (if you have a backport). In modern Java, they are a standard tool.
One subtlety is that constructor references work with any functional interface, not just the ones in java.util.function. If you define a custom interface with a single abstract method, you can use a constructor reference as long as the method signature matches. This is useful when you need a factory with a specific semantic name.
For example, you might define a PersonFactory interface:
@FunctionalInterface interface PersonFactory { Person create(String name, int age); }
Then you can use Person::new as the implementation:
PersonFactory factory = Person::new; Person p = factory.create("Bob", 25);
This keeps the code expressive and ties the factory to the constructor directly.