Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
70: The Streams API: Creating Streams
A few years ago, I was reviewing a pull request from a developer who was building a reporting module for a logistics platform. He had written a method to extract a list of "high-priority" shipment IDs from a massive list of order objects. The code was a wall of boilerplate: he initialized a new ArrayList, started a for-each loop, added an if statement to check the priority, and then manually added the ID to the new list. It was 20 lines of code to do one simple thing. I remember telling him, "You're spending all your time managing the plumbing instead of the data." When I showed him how to simply call .stream() on his collection, the look of relief on his face was genuine. He finally stopped worrying about the how and started focusing on the what.
Turning Collections into Pipelines
The most common way you'll encounter streams is by deriving them from an existing collection. Since Collection is a super-interface for List and Set, almost any group of objects you're already working with can be converted into a stream immediately. You just call the .stream() method.
List<String> employees = List.of("Alice", "Bob", "Charlie", "Diana");
Stream<String> employeeStream = employees.stream();
It's important to realize that calling .stream() doesn't actually move the data or create a copy of the list. It just creates a "view" or a pipeline that allows you to process those elements sequentially. If you modify the original list while the stream is running, you're asking for a ConcurrentModificationException, so I'd suggest treating your source collection as read-only once the stream starts.
Handling Arrays and Static Values
Not everything in Java is a Collection. If you're dealing with a raw array, you can't just call .stream() because arrays don't have methods. Instead, you use the Arrays utility class. I've seen people try to wrap arrays in a list just to get a stream, but that's an unnecessary detour.
String[] tags = {"Java", "Streams", "Functional"};
Stream<String> tagStream = Arrays.stream(tags);
Sometimes, you don't even have a data structure yet—you just have a few known values you want to process through a pipeline. In those cases, Stream.of() is your best friend. It's a convenience method that takes a varargs input and wraps it into a stream for you. It's much cleaner than creating a temporary list just to stream it.
Stream<Integer> numberStream = Stream.of(10, 20, 30, 40);
Generating Sequences on the Fly
Every now and then, you'll need a stream that isn't based on a fixed set of data. This is where Stream.iterate and Stream.generate come in. I'll warn you right now: these create infinite streams. If you try to collect an infinite stream into a list without a limit, your application will happily eat all your available RAM until it crashes.
Stream.iterate is perfect when the next value depends on the previous one. For example, if you need a stream of even numbers starting from 0, you provide a starting value and a lambda expression to calculate the next step:
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2);
On the other hand, Stream.generate is for when you want a stream of completely independent values, like random numbers or a constant heartbeat signal. You pass it a Supplier, and it just keeps calling that supplier every time the stream asks for a new element.
Stream<Double> randoms = Stream.generate(Math::random);
Just remember: whenever you use iterate or generate, you must eventually use a method like .limit(n) to tell Java when to stop. Otherwise, your pipeline will never end.
📋 Practical Task
Exercise: Building a Product ID Generator
You are tasked with creating a system that generates a sequence of unique Product IDs for a warehouse inventory system. The IDs should follow a specific pattern: they start at 1000 and increase by 1 for every product created.
Write a program that does the following:
- Use
Stream.iterateto create an infinite stream of integers starting at 1000. - Limit the stream to exactly 15 elements (representing the first 15 products).
- Convert these integers into Strings, prefixing each one with "PROD-". (Hint: You can use
.map()or a simple loop to print them, but the core of the generation must be the Stream). - Print each generated Product ID to the console.
Expected Output:
PROD-1000
PROD-1001
...
PROD-1014
There are no comments for now.