Java Programming Language Study Notes
What are Lambda Expressions?
Lambda expressions were introduced in Java 8 to make code shorter, cleaner, and easier to read. They are a concise way to represent an anonymous function, essentially a method without a name, used when you want to pass code as a value.
Why Use Lambda Expressions?
Lambda expressions are used to write less code, avoid creating unnecessary classes, pass behavior (code) as a parameter, and work easily with Collections and Streams.
When Can You Use Lambda Expressions?
Lambda expressions can only be used when you have a Functional Interface. A Functional Interface is defined as an interface with exactly one abstract method.
Lambda Expression Syntax
The basic syntax of a lambda expression is (parameters) -> { body }. The parameters are the inputs to the expression, the arrow separates parameters from the body, and the body contains the code to be executed.
Types of Lambda Parameters
Lambda expressions can have zero parameters (e.g., () -> System.out.println("Hello")), one parameter (e.g., p -> System.out.println(p)), or multiple parameters (e.g., (a, b) -> a + b).
Lambda Expressions with Collections/Streams
Lambda expressions are particularly useful when working with Java Collections and Streams. They allow for concise operations like filtering, mapping, and iterating over collections.
Built-in Functional Interfaces
Java provides several built-in functional interfaces, including Predicate (tests a condition), Consumer (accepts input and performs an action), Supplier (provides output), and Comparator (compares objects).
What are Method References?
A method reference is a shortcut for a lambda expression that calls an existing method. It's used when your lambda expression's sole purpose is to invoke a pre-defined method.
Types of Method References
There are four types of method references: Static Method Reference (ClassName::method), Instance Method Reference of a Particular Object (obj::method), Instance Method Reference of an Arbitrary Object of a Type (ClassName::method), and Constructor Reference (ClassName::new).
Static Method Reference Example
A static method reference is used when the method being referenced is static. The syntax is ClassName::staticMethod. For example, Collections.sort(personList, Geeks::compareByName) if compareByName is a static method in the Geeks class.
Instance Method Reference (Particular Object)
This type is used when you want to call a method on a specific, already existing object. The syntax is object::method. For example, cmp::compareByName, where cmp is an object with a compareByName method.
Instance Method Reference (Arbitrary Object)
This reference is used when you want to call a method on any object of a class, not a specific one. The syntax is ClassName::method. An example is String::compareToIgnoreCase, used for sorting strings.
Constructor Reference
A constructor reference is used to create new objects. The syntax is ClassName::new. For instance, Person::new can be used to create new Person objects, often within stream operations.
What is a Stream?
A Stream in Java is a sequence of elements that can be processed in a functional style. Streams are lazy, meaning intermediate operations don't execute until a terminal operation is called, and they are single-use; once consumed, they cannot be reused.
Creating Streams
Streams can be created from various sources, including Collections (list.stream()), Arrays (Arrays.stream(arr)), specific values (Stream.of(1, 2, 3)), and infinite sequences (Stream.iterate(...).limit(...)).
Stream Pipeline
A stream pipeline consists of a Source, Intermediate Operations, and a Terminal Operation. The source provides the data, intermediate operations transform the stream (e.g., filter, map), and the terminal operation produces a result (e.g., forEach, collect).
Useful Intermediate Operations
Key intermediate operations include filter(p) to keep matching elements, map(f) to transform elements, distinct() to remove duplicates, sorted() to sort elements, and limit(n)/skip(n) to control the number of elements.
Useful Terminal Operations
Important terminal operations are forEach(consumer) to perform an action on each element, collect(toList()) to gather elements into a collection, count() to get the number of elements, reduce(...) to combine elements, and findFirst()/findAny() to retrieve an element.
Types of Streams
Streams can be sequential (default, single-threaded), parallel (multi-threaded for potential performance gains), infinite (requiring a limit), or primitive streams (IntStream, LongStream, DoubleStream) for efficient primitive type processing.
Stream vs. Collection
A Collection stores data (like List or Set), while a Stream processes data. Streams are pipelines, not storage mechanisms, and are typically derived from collections.
Streams and Files
Java Streams can be used to process files efficiently. Using try-with-resources ensures that file streams are automatically closed. For example, reading lines from a file and filtering them.
Stream Gotchas
Remember that streams are single-use. Intermediate operations are lazy. Avoid side-effects within stream operations, especially with parallel streams, and prefer collect() for thread-safe accumulation.