Skip to content

Instantly share code, notes, and snippets.

@AryanGitHub
Created June 8, 2023 17:43
Show Gist options
  • Save AryanGitHub/79e9e67470585c6054b4906a2d0590fe to your computer and use it in GitHub Desktop.
Save AryanGitHub/79e9e67470585c6054b4906a2d0590fe to your computer and use it in GitHub Desktop.
Generics in Java
  1. Boxing and Unboxing can make programs faster when used correctly.

Take this example!

public static int sum (List<Integer> ints) {
	int s = 0;
	for (int n : ints) { s += n; }
	return s;
}

Why does the argument have type List and not List? Because type parameters must always be bound to reference types, not primitive types. Why does the result have type int and not Integer? Because result types may be either primitive or reference types, and it is more efficient to use the former than the latter. Unboxing occurs when each Integer in the list ints is bound to the variable n of type int.

We could rewrite the method, replacing each occurrence of int with Integer:

public static Integer sumInteger(List<Integer> ints) {
	Integer s = 0;
	for (Integer n : ints) { s += n; }
	return s;
}

This code compiles but performs a lot of needless work. Each iteration of the loop unboxes the values in s and n, performs the addition, and boxes up the result again. With Sun’s current compiler, measurements show that this version is about 60 percent slower than the original.

  1. Boxed values may be cached.

Caching is required when boxing an int or short value between–128 and 127, a char value between '\u0000' and '\u007f', a byte, or a boolean; and caching is permitted when boxing other values. Hence, in contrast to our earlier example, we have the following:

List<Integer> smalls = Arrays.asList(1,2,3);
assert sumInteger(smalls) == sum(smalls);
assert sumInteger(smalls) == sumInteger(smalls); // not recommended

This is because 6 is smaller than 128, so boxing the value 6 always returns exactly the same object. In general, it is not specified whether boxing the same value twice should return identical or distinct objects, so the inequality assertion shown earlier may either fail or succeed depending on the implementation. Even for small values, for which == will compare values of type Integer correctly, we recommend against its use. It is clearer and cleaner to use equals rather than == to compare values of reference type, such as Integer or String.

  1. For each

Here, again, is our code that computes the sum of a list of integers.

List<Integer> ints = Arrays.asList(1,2,3);
int s = 0;
for (int n : ints) { s += n; }
assert s == 6;

The loop in the third line is called a foreach loop even though it is written with the keyword for. It is equivalent to the following:

for (Iterator<Integer> it = ints. iterator(); it.hasNext(); ) {
    int n = it.next();
    s += n;
}

The emphasized code corresponds to what was written by the user, and the unemphasized code is added in a systematic way by the compiler. It introduces the variable it of type Iterator<Integer> to iterate over the list ints of type List<Integer>. In general, the compiler invents a new name that is guaranteed not to clash with any name already in the code. Note that unboxing occurs when the expression it.next() of type Integer is assigned to the variable n of type int.

The foreach loop can be applied to any object that implements the interface Iterable<E> (in package java.lang), which in turn refers to the interface Iterator<E> (in package java.util). These define the methods iterator, hasNext, and next, which are used by the translation of the foreach loop (iterators also have a method remove, which is not used by the translation):

interface Iterable<E> {
    public Iterator<E> iterator();
}

interface Iterator<E> {
    public boolean hasNext();
    public E next();
    public void remove();
}

All collections, sets, and lists in the Collections Framework implement the Iterable<E> interface; and classes defined by other vendors or users may implement it as well.

  1. Generic Methods

Here is a method that accepts an array of any type and converts it to a list:

class Lists {
    public static <T> List<T> toList(T[] arr) {
        List<T> list = new ArrayList<T>();
        for (T elt : arr) 
            list.add(elt);
        return list;
    }
}

The static method toList accepts an array of type T[] and returns a list of type List<T>, and does so for any type T. This is indicated by writing <T> at the beginning of the method signature, which declares T as a new type variable. A method which declares a type variable in this way is called a generic method. The scope of the type variable T is local to the method itself; it may appear in the method signature and the method body, but not outside the method.

  1. vararg

The vararg feature permits a special, more convenient syntax for the case in which the last argument of a method is an array. To use this feature, we replace T[] with T… in the method declaration:

class Lists {
    public static <T> List<T> toList(T... arr) {
        List<T> list = new ArrayList<T>();
        for (T elt : arr) list.add(elt);
        return list;
    }
}

Now the method may be invoked as follows:

List<Integer> ints = Lists.toList(1, 2, 3);
List<String> words = Lists.toList("hello", "world");

This is just shorthand for what we wrote above. At run time, the arguments are packed into an array which is passed to the method, just as previously.

Any number of arguments may precede a last vararg argument. Here is a method that accepts a list and adds all the additional arguments to the end of the list:

public static <T> void addAll(List<T> list, T... arr) {
    for (T elt : arr) list.add(elt);
}

Whenever a vararg is declared, one may either pass a list of arguments to be implicitly packed into an array, or explicitly pass the array directly. Thus, the preceding method may be invoked as follows:

List<Integer> ints = new ArrayList<Integer>();
Lists.addAll(ints, 1, 2);
Lists.addAll(ints, new Integer[] { 3, 4 });
  1. Assertions

Assertions are used in code to verify that certain conditions are true. They are helpful for debugging and catching logical errors. In Java, the assert statement is used for this purpose.

Here is an example of code that uses assertions:

int[] ints = new int[]{1, 2, 3};
int s = 0;
for (int i = 0; i < ints.length; i++) {
    s += ints[i];
}
assert s == 6;

In this example, we have an array of integers ints and a variable s initialized to 0. We iterate over the array and add each element to s. Finally, we assert that the value of s is equal to 6. If assertions are enabled and the expression evaluates to false, an AssertionError will be thrown.

To enable assertions in Java, you can invoke the JVM with the -ea or -enableassertions flag. This ensures that assertions are executed and checked during runtime. If assertions are not enabled, they are ignored.

It's important to note that assertions should only be used for conditions that you expect to be true. They should not have any side effects that affect the behavior of non-assertion code. When checking for conditions that might not hold, such as validating method arguments, it's better to use conditionals and throw explicit exceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment