Skip to main content

Advanced

Annotations

Annotations are special types that act as metadata:

  • It has no direct affect on target behavior
  • Annotation have to interpreted by things like tools (IntelliJ), execution environment (XML and JSON processors), any program
  • Can be applied to a type or its members

Common Java core platform annotations

  • @Override: indicate we override an inherited method
  • @Deprecated: indicate which method is no longer used
  • @SuppressWarnings: tell compiler to not generate some kind of warnings (e.g. @SuppressWarnings("deprecation"))

Others:

Generics

Using <> (diamond operator).

info

A generic type is a type with type parameters, which is instantiated into a parameterized type by supplying type arguments to fill in its type parameters.

info

Type Inference: compiler automatically knows the type without us specifying it.

Generic Class

Example

public interface TreeNode<T> {
// after decalre `TreeNode<T>`, all its members can use `T` as it's an actual type
T getValue();
TreeNode<T> getLeft();
TreeNode<T> getRight();
}

public class LeafNode<T> implements TreeNode<T> {
private final T value;

public LeafNode(T value) { this.value = value; }
@Override public T getValue() { return value; }
@Override public TreeNode<T> getLeft() { return null; }
@Override public TreeNode<T> getRight() { return null; }
@Override public String toString() { return String.format("[%s]", value); }
}

public class InnerNode<T> implements TreeNode<T> {
private final TreeNode<T> left, right;

public InnerNode(TreeNode<T> left, TreeNode<T> right) {
this.left = left;
this.right = right;
}
@Override public T getValue() { return null; }
@Override public TreeNode<T> getLeft() { return left; }
@Override public TreeNode<T> getRight() { return right; }
@Override public String toString() { return String.format("{%s, %s}", left, right); }
}
// main
public static void main(String[] args){
var three = new LeafNode<Integer>(3);
// type inference: compiler can automatically understand as well
var five = new LeafNode<>(5);
var tree = new InnerNode<>(new LeafNode<>(2), new InnerNode<>(three, five));
System.out.println(tree);
}

Generic Types

tóm tắt

Nôm na là với type bình thường, mình gọi "instantiate" là create (instance of) new object, còn với generic type, create new instance of object được gọi là "instantiating generic type into parameterized type"

Dùng toán tử diamond <>, cứ coi nó giống như new, dùng để create a new object from a parameterized type

InnerNode is a generic type. Because it has type parameter <T> (public class InnerNode<T> implements TreeNode<T>).

Type Parameters

A placeholder for an actual concrete type:

  • Usually use single capital letter for convention. <T> stands for type, <K, V> stands for key and value.
  • Cannot use primitive type as type arguments \rightarrow use wrapper type.
  • Cannot use on anonymous inner classes, enums and exception classes.

Type Arguments

var three = new LeafNode<Integer>(3);

Integer is type argument. Above, you use a generic type with specific type arguments, which is "instantiating a generic type into a parameterized type".

Generic type\rightarrow Parameterized type
List<T>\rightarrow List<String>
Generic type\rightarrow Concrete parameterized type

Instantiate at Value and Type level

They are the same at 2 levels, just different at the way of speaking. The term "instantiating" at level of values means create a new object. At type level:

Value levelType level
Define methods/constructors with value parameters. E.g. int a, String bDefine generic types with type parameters. E.g. <K, V>
Call methods/constructors with arguments = actual values. E.g. (23, "hello")Supply type arguments = actual types. E.g. <Integer, String>
This is instantiate a type into an objectThis is instantiate a generic type into a parameterized type

Read again about Instantiate.

Generic Methods

Motivation

// main
Person ronaldo = new Person("Ronaldo", 43);
Person messi = new Person("Messi", 34);
List<Person> players = new ArrayList<>();
players.add(ronaldo);
players.add(messi);
// suppose we want to find youngest one

final Person youngest = (Person) min(players, new AgeComparator());

public static Object min(List values, Comparator comparator) {
if(values.isEmpty()) { throw Exception("..."); }
Object lowestElement = values.get(0);
for (int i = 1; i < values.size(); i++) {
final Object element = values.get(i);
if (comparator.compare(element, lowestElement) < 0) {
lowestElement = element;
}
}
return lowestElement;
}
/**
Problems:
- We have to cast `(Person)` because we're not using generic
- Compiler don't warn ClassCastException at compile-time -> fail at runtime. e.g.
**/
final Person youngest = (Person) min(players, new Comparator<Integer>(){ // error in runtime
publbic int compare(final Integer o1, final Integer o2) { return 0; }
});

/**
declare the part `...<T> return type...` first, then you can use it in `List<T>` and `Comparator<T>`
something like `public static foo(T value)` is not working
**/
public static <T> T min (List<T> values, Comparator<T> comparator {
// replace all Object with T
}
// now you can even use method reference
List<Integer> numbers = new ArrayList<>();
numbers.add(1); numbers.add(2); numbers.add(3);
System.out.println(min(numbers, Integer::compare));

Usage

Example

public record Pair<T, U>(T first, U second) {
/**
- <V, W>: type param, comes before return type
we need this even inside record has type params (Pair<T, U>)
because this is static method (cannot access instance/instantiating of their enclosing type)
- Pair<V, W>: return type
- of: method name
**/
public static <V ,W> Pair<V, W> of(V first, W second) {
return new Pair<>(first, second);
}

/**
this is regular non-generic methods
because it can access the type parameters of the enclosing record
unlike static, it will always be called on a particular object
**/
public Pair<T, U> withFirst(T newFirst) {
return Pair.of(newFirst, second);
}

/**
Read more at Functional Interface to know about BiFunction
BiFunction represents an arbitrary function with 2 paramters and returns a value
here, it can use <T, U> because it's an instance method
**/
public <V, W> Pair<V, W> map(BiFunction<T, U, Pair<V, W>> fn) {
return fn.apply(first, second);
}
}

// main
public static void main(String[] args){
/**
explicit way
`Pair.`: we're calling static method
<Integer, String>: type arguments
**/
var p1 = Pair.<Integer, String>of(1, "one");
/**
implicit way
call `of` withoud type params
we don't even need use <> like usual
**/
var p2 = Pair.of(2, "two");

// short-hand
var p3 = p2.map((left, right) -> Pair.of(right, left);
// long-hard
var p4 = p2.<String, Integer>map((Integer left, String right) -> Pair.<String, Integer>of(right, left));
}

Bounded Type Parameters

Put restrictions on the types that can be used as type arguments. Use extends in type parameter <>.

// note that Comparable itself a generic type
public interface TreeNode<T extends Comparable<T>> { }

/**
T in LeafNode<T> is passed into TreeNode<T>
and compiler checks it may not be subclass of Comparable
**/
public class LeafNode<T> implements TreeNode<T> { }
public class LeafNode<T extends Comparable<T>> implements TreeNode<T> { }
note

TreeNode<T extends Comparable> is less safer than TreeNode<T extends Comparable<T>>, it bypasses the type safety checks of the compiler (note that Comparable is a generic). This is also known as raw type.

/**
this means T is bounded to the Comparable interface, but without a specific type argument
**/
TreeNode<T extends Comparable>
/**
type T must be a subclass of Comparable with the same type argument T
this allows the TreeNode class to use the compareTo method defined in the Comparable interface,
which can be called on objects of type T to compare them with each other.
**/
TreeNode<T extends Comparable<T>>

// method
T left, T right
left.compareTo(right) // works correctly
left.compareTo(new Object()) // if you've not told it what it's generic on, compiler won't warn, but you will get error

Multiple Type Parameter Bounds

Use &, |.

public interface HasId { long id(); }
public interface HasName { String name(); }
public record Product(long id, String name, String description) implements HasId, HasName { }

// make this function more general by sort anything has id and name
static List<String> sortByIdAndExtractNames(List<Product> list) {
return list.stream().sorted(Comparator.comparing(Product::id)).map(Product::name).toList();
}
// add type parameter & extends List<Product> -> List<T>
static <T extends HasId & HasName> List<String> sortByIdAndExtractNames(List<T> list) {
return list.stream().sorted(Comparator.comparing(HasId::id)).map(HasName::name).toList();
}

// main
public static void main(String[] args) {
var products = List.of(
new Product(100346L, "Bread", "Whole-whear loaf"),
new Product(100252L, "Cheese", "Gouda cheese"),
new Product(100123L, "Apples", "Tasty red apples"));
var names = sortByIdAndExtractNames(products);
}

Raw Types

Generic type that is used without type arguments.

List objects = new ArrayList();  // no <...>
objects.add("Hello");
objects.add(123);
String text = (String) objects.get(1); // ClassCastException

TreeNode<T extends Comparable> // no Comparable<T>
T left
left.compareTo(new Object()) // ClassCastException
warning

They are useless, and you should avoid using raw types. They only exist for backward compatibility.

Working with Inheritance

Generic types are invariant. If "a dog is-a animal", so "a list of dogs is a list of animals" is covariance. Generics have this characteristic to prevent bad things happen.

interface Animal {}
record Dog(String name) implements Animal {}
record Cat(String name) implements Animal {}

public static void main(String[] args){
Animal animal = new Dog("Max");
List<Dog> dogs = new ArrayList<>(new Dog("Daisy"));
List<Animal> animals = dogs;
animals.add(new Cat("Luna")); // invariant avoid things like this could happen
}

Wildcards

Motivation: Read this example.

Because of type-safe, generics prevents polymorphism \rightarrow wildcard helps us a little more flexibility but still safety.

warning

A wildcard stands for a particular, but unknown type.

It is not a list that contains objects of arbitrary different types (that would be List<Object>).

Wildcards are used to declare wildcard parameterized types:

Generic typeParameterized type
List<T>\rightarrow Concrete parameterized type: List<String>
List<T>\rightarrow Wildcard parameterized type: List<?>

A wildcard is a way to refer to a family of types. Wildcard Matching:

  • List<String>, List<Integer>, List<Map<?, ?>> matches List<?>
  • List<Dog>, List<Cat>, List<Animal> matches List<? extends Animal>
info

Substitution principle: whenever we pass in argument to a method, we can pass in a subclass/implement of interface.

Java supports subtyping, means if you have a variable of type T and S is a subtype of T, then you can assign an object of type S to the variable.

It also works with wildcard parameterized type.

E.g.

Animal animal = new Dog("Max");  // subtyping
List<Animal> animals = dogs; // generic types are invariant
/**
cái này OK vì subtyping
wildcard là vd che 1 nửa bên thì sẽ không biết chính xác là type gì
(mặc dù nhờ vế bên trái, ta biết đó là type Dog)
**/
List<? extends Animal> anmials = dogs;

/**
List<Animal>: A List that can contain any kind of object of type Animal, it can contain both Dog and Cat objects
List<? extends Animal>: A List of objects of a particular, but unknown type that extends Animal
**/
/**
chứ wildcard không phải là 1 list của nhiều loại object
vd như ở dưới đây cả 2 dòng đều error
The compiler expects us to supply a value that is of the capture type of the wildcard (some unknown type which extends Animal)
but here it's Dog/Cat -> error
**/
animals.add(new Cat("Luna")); // error: Cat cannot be converted to capture#1 of ? extends Animal
animals.add(new Dog("Max")); // error: Dog cannot be converted to capture#1 of ? extends Animal
note

The capture of the wildcard is a particular unknown type that captures wildcard.

Unbounded Wildcard

?: family of all types.

Upper Bounded Wildcard

? extends SomeType: family of types that are subtypes of the specified type, including the type itself.

It is used to get data out of the parameter: covariance.

Lower Bounded Wildcard

? super SomeType: family of types that are super types of the specified type, including the type itself.

It is used to put data into of the parameter: contravariance.

Usage

note
  • Use <T extends > when you want to restrict the and refer the type elsewhere in code.
  • Use <? extends > like a parameter on a method when you want flexibility but don't really need those type parameter.
Problems
// this requires dest and src to have exact same element type
public static <T> void copy(List<T> dest, List<T> src)

// main
List<Dog> dogs = List.of(new Dog("Daisy"), new Dog("Lucky"));
List<Animal> animals = new ArrayList<>();
copy(animals, dogs); // type mismatch error

// use wildcard
// out in
public static <T> void copy(List<? super T> dest, List<? extends T> src)
copy(animals, dogs); // OK

Use when define methods that take parameters of parameterized types (because using concrete type would cause unnecessary restrictions).

  • Upper bounded wildcard for in params
  • Lower bounded wildcard for out params
  • Unbounded wildcard for param that is both in and out. E.g.
// work with list of any type & doesn't do anything with elements of the list than counting
public static int size(List<?> list)
warning

Avoid using wildcards in the return type of a method

More Examples

Wildcard in rescue:

// use generic method
public <T extends Person > void saveAll(List<T> persons) { }
// use wildcard (less clumsy)
public void saveAll(List<Person> persons) { }
public void saveAll(List<? extends Person> persons) { }

// main
List<Player> persons = new ArrayList<>();
persons.add(ronaldo);
persons.add(messi);

// we can do this without error
saveAll(persons);

You cannot add a known things to unknown thing:

List<?> lo;

List<String> ls = new ArrayList<>();
ls.add("vnd");
ls.add("10000");

lo = ls;
System.out.println(lo); // [vnd, 10000]
System.out.println(ls); // [vnd, 10000]

List<?> objs = new ArrayList<>();
/**
this is a list of objects of a specific unknown type
you cannot add elements to this list because the type is unknown
**/
objs.add(1);
objs.add("vnd");

Again, wildcards allows you to pass in a list that contains objects of type Number or any superclass of Number, but it does not guarantee that the list will actually contain any Number objects. So if you use <? extends>, it works, because if it guarantees the list contains only objects that are convertible to Number:

public static void func(List<? super Number> nums) {
for(Number num: nums) { } // nums may include other class that's not Number convertiable
}

public static void func(List<? extends Number> nums){
for (Number num: nums) { }
}

Type Erasure & Limitations

Generics are a compile-time only feature. Generic and parameterized types, type parameters and type arguments do not exist at runtime:

  • Type parameters are replaced by Object or the leftmost bound (if they have bounds)
  • Type arguments are discarded
  • Parameterized types are replaced by raw types
  • Type casts are added where necessary

Type erasure causes limitations in generic:

  • Cannot use primitive type as type arguments: because primitive types are not objects
  • Cannot create a new instance of a type parameter: new T() doesn't work
  • instaceof does not work with non-reifiable types (type information is lost during type erasure, they are parameterized types with at least one concrete or bounded wildcard type argument):
    obj instaceof List<String>  // Error
    obj instanceof List<?> // OK
  • No class literals for parameterized types:
    Class<?> cls = List<String>.class;  // Error
    static class A <T extends B> { } // this is fine
    static class A <? extends B> { }
  • Operations where type safety cannot be guaranteed cause unchecked warnings
  • Cannot overload methods with the same method signature after type erasure:
    // they're the same after type erasure
    void print(List<String> strings) // void print(List strings)
    void print(List<Integer> integers) // void print(List integers)

Heap Pollution

Java's type system is not perfect. There are situations where it can be happen that a variable of a parameterized type refers to an object that is actually of a different type.

Means you can get a value in your heap that's a different type to the one that compiler thinks it should be \rightarrow cause runtime problems.

Working with Array

In Java, generics and arrays don't play nice together.

Working with varargs

Variable arguments are really just syntactic sugar for arrays \rightarrow not good with generics as well.

Lambda Expressions

Functional Interfaces

A functional interface is an interface has exactly 1 abstract method. E.g. Interface Comparator is a functional interface (it only has 1 abstract compare method).

Lambda expression & method reference implements functional interface.

Standard Functional Interfaces

Package java.util.function: commonly used functional interfaces.

  • Function<T, R>: accept T, return R
  • Consumer<T>: accept T, no return
  • Supplier<T>: no args, return value (opposite Consumer)
  • Runnable: no input, no output
  • BiFunction<T, U ,R>: accept T & U, return R
  • BiConsumer<T, U>: accept T & U, no return
  • Predicate<T>: accept T, return boolean
  • BiPredicate<T, U>: accept T & U, return boolean
  • UnaryOperator<T>: accept T, return T
  • BinaryOperator<T>: accept T & T, return T

Lambda Expressions

Lambda expression is an anonymous method. It implements a functional interface. It allows you to pass a block of code as an argument to a method/constructor.

@FunctionalInterface
interface Hello {
public String sayHello();
}

Hello lambdaExp = () -> "Hello Lambda.";
lambdaExp.sayHello() // returns "Hello Lambda."

There's always a functional interface involved which determines the exact type of the lambda expression. E.g:

public boolean replace(Product oldProduct, Product newProduct) {
List<Product> products = new ArrayList<>();
products.replaceAll(product -> {
if(product == oldProduct)
return newProduct;
else
return product;
});
}
/**
`replaceAll(UnaryOperator<E> operator)`
-> the passed-in lambda expression is actually implement an functional interface `UnaryOperator`
**/

You cannot assign a lambda expression to var, must explicitly specify the variable type.

/**
normal syntax:
access modifier - return type - method name - paramater list - body
**/
public int compare(String first, String second) {
return Integer.compare(first.length(), second.length());
}
/**
lambda expression syntax:
parameter list - arrow - body
**/
// the types are optional
(String first, String second) -> {
(first, second) -> Integer.compare(first.length(), second.length());
} // only return doesn't need bracket
/**
implements
public interface Comparator<T> {
int compare(T o1, T o2);
}
**/

Capturing Local Variables

Local variables referenced by lambda expression must be final \rightarrow no allow modification inside or outside lambda expression.

So not always lambda expression is better:

var names = List.of("Ronaldo", "Messi", "Neymar");
var count = 0;
// doesn't work
names.forEach(name -> System.out.println(++count + ": " + name));
// works
for (String name : names) {
System.out.println(++count + ": " + name);
}

Lambda expression & method reference makes it possible to functional programming style in Java: no side effects. You can but you shouldn't, it should be a pure function.

var names = List.of("A", "B", "C");
// normal way
var result1 = new ArrayList<String>();
for(String name: names){
resutl1.add(name.toUpperCase());
}
// side effect way
var result2 = new ArrayList<String>();
names.forEach(name -> result2.add(name.toUpperCase())); // modify names
// no side effect way
var result3 = names.stream().map(name -> name.toUpperCase()).toList();

Method References

Also implements a functional interface. Use :: operator. It can reference to:

  • Static methods: TypeName::staticMethodName
  • Instance methods of particular objects: objectRef::instanceMethodName
  • Instance methods of an arbitrary object of a particular type: TypeName::instanceMethodName
  • Constructor: TypeName::new

Method reference can help use handle parameter passing. Used with collections, such as with forEach():

public class Main {
public static void main(String[] args) {
var names = List.of("Sana", "Momo", "Mina");
// lambda expression
names.forEach(name -> System.out.println(name));
// method reference
names.forEach(System.out::println);

List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle());
shapes.add(new Circle());
// lambda expression
shapes.forEach((shape) -> {
shape.draw();
});
// method reference
shapes.forEach(Shape::draw);
}
}

Java Reflection

Java reflection is a feature that allows a program to inspect and modify the behavior of objects at runtime.

With reflection, a program can obtain information about Class, Field, Method, Constructor, as well as access and modify their values, invoke their methods, and create new instances of the class.

Class<?> clazz = Class.forName("MyClass");

// Create a new instance of the MyClass class using reflection
Object obj = clazz.newInstance();

// Get a reference to the foo field of the MyClass class using reflection
Field field = clazz.getDeclaredField("foo");

// Set the value of the foo field using reflection
field.setAccessible(true);
field.set(obj, "Hello, world!");

// Invoke the bar method of the MyClass class using reflection
Method method = clazz.getDeclaredMethod("bar");
method.setAccessible(true);
method.invoke(obj);

// Get all public methods of the class
Method[] methods = clazz.getMethods();

Varargs

Varargs means variable arity arguments. These are arguments that can take a variable number of values.

There's effectively an array that gets create under the hood, passed over a mythical boundary, and then thrown away afterward.

public static <T> T[] arrayOf(T ... values) {
return values;
}
// main
Integer[] integers = arrayOf(1, 2);
Arrays.toString(integers); // returns [1, 2]

Heap pollution from parameterized vararg type

public static <T> T[] arrayOf(T ... values) { return values; }
public static <T> T[] pair(T t) { return arrayOf(t, t); }

// main
Object[] strs = pair("a");
Arrays.toString(strs) // [a, a]
Integer[] pair = pair(1); // ClassCastException, it's look like an Integer array, but it's actually Object array
Arrays.toString(pair)

Use annotation @SafeVarargs, but use it correctly.