Java programming with assertions and generics Credit: Thinkstock From assertions in JDK 1.4 to the forthcoming lambdas in Java 8, the Java language has evolved considerably since its inception. The next several Java 101 articles present a toolbox of essential Java language features, starting this week with assertions and generics. The Java language is one of the most widely used programming languages in the world. The next several articles in Java 101: The next generation will focus on features added to the Java language from Java 1.4 to Java 8 (and even touch on Java 9). My goal is to introduce you to a toolbox of essential Java language features, with examples demonstrating both why and how they’re used in Java programs. This first article is all about assertions, which were added in Java 1.4, and generics, the first of a handful of important new features introduced in Java 5. Assertions in Java 1.4 Assertions, introduced in Java 1.4, remain one of the most useful and important additions to the Java language. Assertions are used to codify the requirements that render a program correct or not. Assertions test conditions (aka Boolean expressions) for true values, notifying the developer when such conditions are false. Using assertions can greatly increase your confidence in the correctness of your code. The Java Q&A blog Do you have questions about Java programming? Get reliable answers from an experienced Java tutor. Jeff Friesen’s Java Q&A blog is published weekly to address common programming questions from Java beginners and more experienced developers. Assertions are compilable entities that execute at runtime, assuming you’ve enabled them for program testing. You can program assertions to notify you of bugs at the points where the bugs occur, which can greatly reduce the amount of time you would otherwise spend debugging a failing program. Before Java 1.4, developers mostly used comments to document assumptions about code correctness. While useful for documenting code, comments are inferior to assertions as a testing and debugging mechanism. Because the compiler ignores comments, there is no way to use them for bug notification. It’s also common for comments to be unchanged, even when code changes. Implementing assertions Assertions are implemented via the assert statement and java.lang.AssertionError class. This statement begins with the keyword assert and continues with a Boolean expression. An assert statement is expressed syntactically as follows: assert BooleanExpr; If BooleanExpr evaluates to true, nothing happens and execution continues. If the expression evaluates to false, however, AssertionError is instantiated and thrown, as demonstrated in Listing 1. Listing 1. AssertDemo.java (version 1) public class AssertDemo { public static void main(String[] args) { int x = -1; assert x >= 0; } } The assertion in Listing 1 indicates the developer’s belief that variable x contains a value that is greater than or equal to 0. However, this is clearly not the case; the assert statement’s execution results in a thrown AssertionError. Compile Listing 1 (javac AssertDemo.java) and run it with assertions enabled (java -ea AssertDemo). You should observe the following output: Exception in thread “main” java.lang.AssertionError at AssertDemo.main(AssertDemo.java:6) This message is somewhat cryptic in that it doesn’t identify what caused the AssertionError to be thrown. If you want a more informative message, use the assert statement expressed below: assert BooleanExpr : expr; Here, expr is any expression (including a method invocation) that can return a value—you cannot invoke a method with a void return type. A useful expression is a string literal that describes the reason for failure, as demonstrated in Listing 2. Listing 2. AssertDemo.java (version 2) public class AssertDemo { public static void main(String[] args) { int x = -1; assert x >= 0: “x < 0”; } } Compile Listing 2 (javac AssertDemo.java) and run it with assertions enabled (java -ea AssertDemo). This time, you should observe the following slightly expanded output, which includes the reason for the thrown AssertionError: Exception in thread “main” java.lang.AssertionError: x < 0 at AssertDemo.main(AssertDemo.java:6) For either example, running AssertDemo without the -ea (enable assertions) option results in no output. When assertions are not enabled, they are not executed, although they are still present in the classfile. Testing preconditions and postconditions with assertions Assertions are often used to test a program’s preconditions and postconditions: A precondition is a condition that must evaluate to true before the execution of some code sequence. Preconditions ensure that callers keep their contracts with callees. A postcondition is a condition that must evaluate to true after the execution of some code sequence. Postconditions ensure that callees keep their contracts with callers. Preconditions You can enforce preconditions on public constructors and methods by making explicit checks and throwing exceptions when necessary. For private helper methods, you can enforce preconditions by specifying assertions. Consider Listing 3. Listing 3. AssertDemo.java (version 3) import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; class PNG { /** * Create a PNG instance, read specified PNG file, and decode * it into suitable structures. * * @param filespec path and name of PNG file to read * * @throws NullPointerException when <code>filespec is * null */ PNG(String filespec) throws IOException { // Enforce preconditions in non-private constructors and // methods. if (filespec == null) throw new NullPointerException(“filespec is null”); try (FileInputStream fis = new FileInputStream(filespec)) { readHeader(fis); } } private void readHeader(InputStream is) throws IOException { // Confirm that precondition is satisfied in private // helper methods. assert is != null : “null passed to is”; } } public class AssertDemo { public static void main(String[] args) throws IOException { PNG png = new PNG((args.length == 0) ? null : args[0]); } } The PNG class in Listing 3 is the minimal beginning of a library for reading and decoding PNG (portable network graphics) image files. The constructor explicitly compares filespec with null, throwing NullPointerException when this parameter contains null. The point is to enforce the precondition that filespec not contain null. It’s not appropriate to specify assert filespec != null; because the precondition mentioned in the constructor’s Javadoc wouldn’t (technically) be honored when assertions were disabled. (In fact, it would be honored because FileInputStream() would throw NullPointerException, but you shouldn’t depend on undocumented behavior.) However, assert is appropriate in the context of the private readHeader() helper method, which will be completed eventually to read and decode a PNG file’s 8-byte header. The precondition that is always be passed a nonnull value will always hold. Postconditions Postconditions are typically specified via assertions, regardless of whether or not the method (or constructor) is public. Consider Listing 4. Listing 4. AssertDemo.java (version 4) public class AssertDemo { public static void main(String[] args) { int[] array = { 20, 91, -6, 16, 0, 7, 51, 42, 3, 1 }; sort(array); for (int element: array) System.out.printf(“%d “, element); System.out.println(); } private static boolean isSorted(int[] x) { for (int i = 0; i < x.length-1; i++) if (x[i] > x[i+1]) return false; return true; } private static void sort(int[] x) { int j, a; // For all integer values except the leftmost value ... for (int i = 1; i < x.length; i++) { // Get integer value a. a = x[i]; // Get index of a. This is the initial insert position, which is // used if a is larger than all values in the sorted section. j = i; // While values exist to the left of a’s insert position and the // value immediately to the left of that insert position is // numerically greater than a’s value ... while (j > 0 && x[j-1] > a) { // Shift left value—x[j-1]—one position to its right — // x[j]. x[j] = x[j-1]; // Update insert position to shifted value’s original position // (one position to the left). j—; } // Insert a at insert position (which is either the initial insert // position or the final insert position), where a is greater than // or equal to all values to its left. x[j] = a; } assert isSorted(x): “array not sorted”; } } Listing 4 presents a sort() helper method that uses the insertion sort algorithm to sort an array of integer values. I’ve used assert to check the postcondition of x being sorted before sort() returns to its caller. The example in Listing 4 demonstrates an important characteristic of assertions, which is that they’re typically expensive to execute. For this reason, assertions are usually disabled in production code. In Listing 4, isSorted() must scan through the entire array, which can be time-consuming in the case of a lengthy array. Generics in Java 5 In addition to the Java Concurrency Utilities (profiled in June 2013 for this series), Java 5 added eight new language features: generics, typesafe enums, annotations, autoboxing and unboxing, enhanced for loop, static imports, varargs, and covariant return types. I’ll cover all of these Java 5 features over the next two articles, starting here with generics. Generics is a suite of language features that allow types or methods to operate on objects of various types while providing compile-time type safety. Generics address the problem of java.lang.ClassCastExceptions thrown at runtime due to code that is not type safe. Generics in the Java class library Although generics are widely used in the Java Collections Framework, they are not exclusive to it. Generics are also used in other parts of Java’s standard class library, including java.lang.Class, java.lang.Comparable, java.lang.ThreadLocal, and java.lang.ref.WeakReference. Consider the following code fragment, which demonstrates the lack of type safety that was common in Java code before generics were introduced: List doubleList = new LinkedList(); doubleList.add(new Double(3.5)); Double d = (Double) doubleList.iterator().next(); Although the goal of the above program is to store only java.lang.Double objects in the list, nothing prevents other kinds of objects from being stored. For example, you could specify doubleList.add(“Hello”); to add a java.lang.String object. However, when storing another kind of object, the final line’s (Double) cast operator causes ClassCastException to be thrown when confronted with a non-Double object. Because this lack of type safety isn’t detected until runtime, a developer might not be aware of the problem, leaving it to the client to discover. Clearly it would be better to have the compiler detect the problem. Generics aid the compiler by letting the developer mark the list as containing a specific type of object, as follows: List<Double> doubleList = new LinkedList<Double>(); doubleList.add(new Double(3.5)); Double d = doubleList.iterator().next(); List<Double> now reads “List of Double”. List is a generic interface, expressed as List<E>, that takes a Double type argument, which is also specified when creating the actual object. The compiler can now enforce type correctness when adding an object to the list—for instance, the list could store Doubles only. This enforcement removes the need for the (Double) cast. Discovering generic types A generic type is a class or interface that introduces a set of parameterized types via a formal type parameter list, which is a comma-separated list of type parameter names between a pair of angle brackets. Generic types adhere to the following syntax: class identifier<formalTypeParameterList> { // class body } interface identifier<formalTypeParameterList> { // interface body } The Java Collections Framework offers many examples of generic types and their parameter lists. For example, java.util.Set<E> is a generic type with <E> as its formal type parameter list and E as this list’s solitary type parameter. java.util.Map<K, V> is another example. Naming type parameters Java programming convention dictates that type parameter names be single uppercase letters, such as E for element, K for key, V for value, and T for type. If possible, avoid using a meaningless name like “P”—java.util.List<E> means a list of elements, but what could you possibly mean by List<P>? A parameterized type is a generic type instance where the generic type’s type parameters are replaced with actual type arguments (type names). For example, Set<String> is a parameterized type where String is the actual type argument replacing type parameter E. The Java language supports the following kinds of actual type arguments: Concrete type: A class or other reference type name is passed to the type parameter. For example, in List<Animal>, Animal is passed to E. Concrete parameterized type: A parameterized type name is passed to the type parameter. For example, in Set<List<Shape>>, List<Shape> is passed to E. Array type: An array is passed to the type parameter. For example, in Map<String, String[]>, String is passed to K and String[] is passed to V. Type parameter: A type parameter is passed to the type parameter. For example, in class Container<E> { Set<E> elements; }, E is passed to E. Wildcard: The question mark (?) is passed to the type parameter. For example, in Class<?>, ? is passed to T. Each generic type implies the existence of a raw type, which is a generic type without a formal type parameter list. For example, Class is the raw type for Class<T>. Unlike generic types, raw types can be used with any kind of object. Declaring and using generic types Declaring a generic type involves specifying a formal type parameter list and using these type parameters throughout its implementation. Using the generic type involves passing actual type arguments to its type parameters when instantiating the generic type. See Listing 5. Listing 5. GenDemo.java (version 1) class Container<E> { private E[] elements; private int index; Container(int size) { elements = (E[]) new Object[size]; index = 0; } void add(E element) { elements[index++] = element; } E get(int index) { return elements[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { Container<String> con = new Container<String>(5); con.add(“North”); con.add(“South”); con.add(“East”); con.add(“West”); for (int i = 0; i < con.size(); i++) System.out.println(con.get(i)); } } Listing 5 demonstrates generic type declaration and usage in the context of a simple container type that stores objects of the appropriate argument type. To keep the code simple, I’ve omitted error checking. The Container class declares itself to be a generic type by specifying the <E> formal type parameter list. Type parameter E is used to identify the type of stored elements, the element to be added to the internal array, and the return type when retrieving an element. The Container(int size) constructor creates the array via elements = (E[]) new Object[size];. If you’re wondering why I didn’t specify elements = new E[size];, the reason is that it isn’t possible: doing so could lead to a ClassCastException. Compile Listing 5 (javac GenDemo.java). The (E[]) cast causes the compiler to output a warning about the cast being unchecked. It flags the possibility that downcasting from Object[] to E[] might violate type safety because Object[] can store any type of object. Note, however, that there’s no way to violate type safety in this example. It’s simply not possible to store a non-E object in the internal array. I’ll show you how to suppress this warning message in a future article. Execute java GenDemo to run this application. You should observe the following output: North South East West Bounding type parameters The E in Set<E> is an example of an unbounded type parameter because you can pass any actual type argument to E. For example, you can specify Set<Marble>, Set<Employee>, or Set<String>. Sometimes, you’ll want to restrict the types of actual type arguments that can be passed to a type parameter. For example, perhaps you want to restrict a type parameter to accept only Employee and its subclasses. You can limit a type parameter by specifying an upper bound, which is a type that serves as the upper limit on the types that can be passed as actual type arguments. Specify the upper bound via reserved word extends followed by the upper bound’s type name. For example, class Employees<E extends Employee> restricts the types that can be passed to Employees to Employee or a subclass (e.g., Accountant). Specifying new Employees<Accountant> would be legal, whereas new Employees<String> would be illegal. You can assign more than one upper bound to a type parameter. However, the first bound must always be a class, and the additional bounds must always be interfaces. Each bound is separated from its predecessor by an ampersand (&). Check out Listing 6. Listing 6. GenDemo.java (version 2) import java.math.BigDecimal; import java.util.Arrays; abstract class Employee { private BigDecimal hourlySalary; private String name; Employee(String name, BigDecimal hourlySalary) { this.name = name; this.hourlySalary = hourlySalary; } public BigDecimal getHourlySalary() { return hourlySalary; } public String getName() { return name; } public String toString() { return name+”: “+hourlySalary.toString(); } } class Accountant extends Employee implements Comparable<Accountant> { Accountant(String name, BigDecimal hourlySalary) { super(name, hourlySalary); } public int compareTo(Accountant acct) { return getHourlySalary().compareTo(acct.getHourlySalary()); } } class SortedEmployees<E extends Employee & Comparable<E>> { private E[] employees; private int index; SortedEmployees(int size) { employees = (E[]) new Employee[size]; int index = 0; } void add(E emp) { employees[index++] = emp; Arrays.sort(employees, 0, index); } E get(int index) { return employees[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { SortedEmployees<Accountant> se = new SortedEmployees<Accountant>(10); se.add(new Accountant(“John Doe”, new BigDecimal(“35.40”))); se.add(new Accountant(“George Smith”, new BigDecimal(“15.20”))); se.add(new Accountant(“Jane Jones”, new BigDecimal(“25.60”))); for (int i = 0; i < se.size(); i++) System.out.println(se.get(i)); } } Listing 6’s Employee class abstracts the concept of an employee that receives an hourly wage. This class is subclassed by Accountant, which also implements Comparable<Accountant> to indicate that Accountants can be compared according to their natural order, which happens to be hourly wage in this example. The java.lang.Comparable interface is declared as a generic type with a single type parameter named T. This class provides an int compareTo(T o) method that compares the current object with the argument (of type T), returning a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. The SortedEmployees class lets you store Employee subclass instances that implement Comparable in an internal array. This array is sorted (via the java.util.Arrays class’s void sort(Object[] a, int fromIndex, int toIndex) class method) in ascending order of the hourly wage after an Employee subclass instance is added. Compile Listing 6 (javac GenDemo.java) and run the application (java GenDemo). You should observe the following output: George Smith: 15.20 Jane Jones: 25.60 John Doe: 35.40 What about lower bounds? You cannot specify a lower bound for a generic type parameter. To understand why I recommend reading Angelika Langer’s Java Generics FAQs on the topic of lower bounds, which she says “would be confusing and not particularly helpful.” Considering wildcards Let’s say you want to print out a list of objects, regardless of whether these objects are strings, employees, shapes, or some other type. Your first attempt might look like what’s shown in Listing 7. Listing 7. GenDemo.java (version 3) import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenDemo { public static void main(String[] args) { List<String> directions = new ArrayList<String>(); directions.add(“north”); directions.add(“south”); directions.add(“east”); directions.add(“west”); printList(directions); List<Integer> grades = new ArrayList<Integer>(); grades.add(new Integer(98)); grades.add(new Integer(63)); grades.add(new Integer(87)); printList(grades); } static void printList(List<Object> list) { Iterator<Object> iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } } It seems logical that a list of strings or a list of integers is a subtype of a list of objects, yet the compiler complains when you attempt to compile this listing. Specifically, it tells you that a list-of-string cannot be converted to a list-of-object, and similarly for a list-of-integer. The error message you’ve received is related to the fundamental rule of generics: For a given subtype x of type y, and given G as a raw type declaration, G is not a subtype of G. According to this rule, although String and java.lang.Integer are subtypes of java.lang.Object, it’s not true that List<String> and List<Integer> are subtypes of List<Object>. Why do we have this rule? Remember that generics are designed to catch type-safety violations at compile time, which is helpful: without generics, you are much more likely to be called in to work at 2 a.m. because your Java program has thrown a ClassCastException and crashed! As a demonstration, let’s assume that List<String> was a subtype of List<Object>. If this was true, you might end up with the following code: List<String> directions = new ArrayList<String>(); List<Object> objects = directions; objects.add(new Integer()); String s = objects.get(0); This code fragment creates a list of strings based on an array list. It then upcasts this list to a list of objects (which isn’t legal, but for now just pretend it is). Next, it adds an integer to the list of objects, which violates type safety. The problem occurs in the final line, which throws ClassCastException because the stored integer cannot be cast to a string. Without generics, your only option to prevent such a violation of type safety would be to pass an object of type List<Object> to the printList() method in Listing 7, which wouldn’t be very useful. With generics, however, you can use wildcards to solve the problem, as shown in Listing 8. Listing 8. GenDemo.java (version 4) import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenDemo { public static void main(String[] args) { List<String> directions = new ArrayList<String>(); directions.add(“north”); directions.add(“south”); directions.add(“east”); directions.add(“west”); printList(directions); List<Integer> grades = new ArrayList<Integer>(); grades.add(new Integer(98)); grades.add(new Integer(63)); grades.add(new Integer(87)); printList(grades); } static void printList(List<?> list) { Iterator<?> iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } } In Listing 8 I use a wildcard (the ? symbol) in place of Object in printList()’s parameter list and body. Because this symbol stands for any type, it’s legal to pass List<String> and List<Integer> to this method. Compile Listing 8 (javac GenDemo.java) and run the application (java GenDemo). You should observe the following output: north south east west 98 63 87 Discovering generic methods Now say you want to copy a list of objects to another list subject to a filter. You might consider declaring a void copy(List<Object> src, List<Object> dst, Filter filter) method, but this method would only be able to copy Lists of Objects and nothing else. To pass source and destination lists of arbitrary type, you need to use the wildcard for a type placeholder. For example, consider the following copy() method: void copy(List<?> src, List<?> dest, Filter filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } This method’s parameter list is correct, but there’s a problem. According to the compiler, dest.add(src.get(i)); violates type safety. The ? implies that any kind of object can be the list’s element type, and it’s possible that the source and destination element types are incompatible. For example, if the source list was a List of Shape and the destination list was a List of String, and copy() was allowed to proceed, ClassCastException would be thrown when attempting to retrieve the destination list’s elements. You could partially solve this problem by providing upper and lower bounds for the wildcards, as follows: void copy(List<? extends String> src, List<? super String> dest, Filter filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } You can provide an upper bound for a wildcard by specifying extends followed by a type name. Similarly, you can supply a lower bound for a wildcard by specifying super followed by a type name. These bounds limit the types that can be passed as actual type arguments. In the example, you can interpret ? extends String as any actual type argument that happens to be String or a subclass. Similarly, you can interpret ? super String as any actual type argument that happens to be String or a superclass. Because String is final, which means that it cannot be extended, only source lists of String objects and destination lists of String or Object objects can be passed, which isn’t very useful. You can fully solve this problem by using a generic method, which is a class or instance method with a type-generalized implementation. A generic method declaration adheres to the following syntax: <formalTypeParameterList> returnType identifier(parameterList) A generic method’s formal type parameter list precedes its return type. It consists of type parameters and optional upper bounds. A type parameter can be used as the return type and can appear in the parameter list. Listing 9 demonstrates how to declare and invoke a generic copy() method. Listing 9. GenDemo.java (version 5) import java.util.ArrayList; import java.util.List; public class GenDemo { public static void main(String[] args) { List<Integer> grades = new ArrayList<Integer>(); Integer[] gradeValues = { new Integer(96), new Integer(95), new Integer(27), new Integer(100), new Integer(43), new Integer(68) }; for (int i = 0; i < gradeValues.length; i++) grades.add(gradeValues[i]); List<Integer> failedGrades = new ArrayList<Integer>(); copy(grades, failedGrades, new Filter<Integer>() { public boolean accept(Integer grade) { return grade.intValue() <= 50; } }); for (int i = 0; i < failedGrades.size(); i++) System.out.println(failedGrades.get(i)); } static <T> void copy(List<T> src, List<T> dest, Filter<T> filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } } interface Filter<T> { boolean accept(T o); } In Listing 9 I’ve declared a <T> void copy(List<T> src, List<T> dest, Filter<T> filter) generic method. The compiler notes that the type of each of the src, dest, and filter parameters includes the type parameter T. This means that the same actual type argument must be passed during a method invocation, and the compiler infers this argument by examining the invocation. If you compile Listing 9 (javac GenDemo.java) and run the application (java GenDemo) you should observe the following output: 27 43 What’s controversial about generics in the Java language? While generics as such might not be controversial, their particular implementation in the Java language has been. Generics were implemented as a compile-time feature that amounts to syntactic sugar for eliminating casts. The compiler throws away a generic type or generic method’s formal type parameter list after compiling the source code. This “throwing away” behavior is known as erasure. Other examples of erasure in generics include inserting casts to the appropriate types when code isn’t type correct, and replacing type parameters by their upper bounds (such as Object). More generics controversy Generics are controversial for reasons other than erasure. See the StackOverflow.com topic “Why do some claim that Java’s implementation of generics is bad?” for more discussion, including the suggestion that wildcards can be confusing and the fact that generics do not support value types (for instance, you cannot specify List<int>). Using erasure leads to several limitations: With one exception, the instanceof operator cannot be used with parameterized types. The exception is an unbounded wildcard. For example, you cannot specify Set<Shape> shapes = null; if (shapes instanceof ArrayList<Shape>) {}. Instead, you need to change the instanceof expression to shapes instanceof ArrayList<?>, which demonstrates an unbounded wildcard. Alternatively, you could specify shapes instanceof ArrayList, which demonstrates a raw type (and which is the preferred use). The compiler transforms generic code into non-generic code, which is stored in the classfile. Some developers have pointed out that erasure prevents you from using reflection to obtain generics information, which isn’t present in the classfile. In “Java Reflection: Generics” developer Jakob Jenkov points out a few cases where generics information is stored in a classfile, and this information can be accessed reflectively. You cannot use type parameters in array-creation expressions; for example elements = new E[size];. The compiler will report a generic array creation error message if you try to do so. Given erasure’s limitations, you might wonder why generics were implemented with erasure. The reason is simple: the Java compiler was refactored to use erasure so that generic code could interoperate with legacy Java code, which isn’t generic. Without that backward compatibility, legacy Java code would fail to compile in a Java compiler supporting generics. Conclusion to Part 1 The Java language has evolved through the addition of many new features. In this article I’ve shown you how to use assertions to increase your confidence in the correctness of your code, and how to use generics to eliminate ClassCastExceptions. By using assertions and generics, you can craft code that’s much more reliable and minimize code failure at runtime, as well as the associated headache of dealing with angry clients. Java 5 was a pivotal release in the history of the Java platform, and while generics were more controversial than some other features, they weren’t necessarily more important. My next article will introduce two more essential features added to the language in Java 5: typesafe enums and annotations. Until then, check out the source code for this article, which contains more tips and examples for programming with assertions and generics. Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and Android. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for JavaWorld, informIT, Java.net, DevSource, and SitePoint. Jeff can be contacted via his website at TutorTutor.ca. Related content feature Python threading and subprocesses explained Python lets you parallelize workloads using threads, subprocesses, or both. Here's what you need to know about Python's thread and process pools and Python threads after Python 3.13. By Serdar Yegulalp Oct 30, 2024 9 mins Concurrency Python Programming Languages feature Get started with the free-threaded build of Python 3.13 Want to learn the ropes of the new "no-GIL" build and true parallelism in Python? Here's where to start. By Serdar Yegulalp Oct 16, 2024 6 mins Concurrency Python Programming Languages how-to Thread behavior in the JVM The JVM does what it wants to do, so how can you predict the order of thread execution? By Rafael del Nero Jun 27, 2024 11 mins Core Java Concurrency Java how-to Intro to virtual threads: A new approach to Java concurrency Virtual threads take the responsibility for allocating system resources out of your application code and into the JVM. Here's a first look at virtual threads in Java 21. By Matthew Tyson Nov 08, 2023 8 mins Concurrency Java Programming Languages Resources Videos