Transitioning from C# LINQ to Java Streams

Published on

Last Updated on

Estimated Reading Time: 12 min

If you're transitioning from C# to Java, the task of mapping C# LINQ functions to Java Streams can seem challenging While the core concepts bear similarities, unravelling the nuances of syntax and functionality is crucial for success.

In this blog post, we will explore the Java equivalents for C# LINQ functions.

Operations

Select / map

Transforms each element of a sequence based on a given function.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x).ToList();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(x -> x * x)
                                      .collect(Collectors.toList());

Output

[1, 4, 9, 16, 25]

Where / filter

Filters a sequence based on a specified condition.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(x => x % 2 == 0).ToList();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(x -> x % 2 == 0)
                                   .collect(Collectors.toList());

Output

[2, 4]

OrderBy / sorted

Sorts the elements of a sequence in ascending order based on a specified key.

C#

var numbers = new List<int> { 3, 1, 4, 2, 5 };
var sortedNumbers = numbers.OrderBy(x => x).ToList();

Java

List<Integer> numbers = Arrays.asList(3, 1, 4, 2, 5);
List<Integer> sortedNumbers = numbers.stream()
                                     .sorted()
                                     .collect(Collectors.toList());

Output

[1, 2, 3, 4, 5]

OrderByDescending / sorted

Sorts the elements of a sequence in descending order based on a specified key.

C#

var numbers = new List<int> { 5, 2, 9, 1, 7 };
var sortedNumbers = numbers.OrderByDescending(n => n);

Java

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);

List<Integer> sortedNumbers = numbers.stream()
                                     .sorted(Comparator.reverseOrder())
                                     .collect(Collectors.toList());

Output

[9, 7, 5, 2, 1]

GroupBy / groupingBy

Groups the elements of a sequence based on a specified key.

C#

var people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 30 },
    new Person { Name = "Charlie", Age = 25 }
};
var peopleByAge = people.GroupBy(p => p.Age);

Java

List<Person> people = Arrays.asList(
        new Person("Alice", 25),
        new Person("Bob", 30),
        new Person("Charlie", 25)
);
Map<Integer, List<Person>> peopleByAge = people.stream()
                                               .collect(Collectors.groupingBy(Person::getAge));

Output

{
    25: [{ Name: "Alice", Age: 25 }, { Name: "Charlie", Age: 25 }],
    30: [{ Name: "Bob", Age: 30 }]
}

Join / join

Combines two sequences based on matching keys.

C#

var persons = new List<Person>
{
    new Person { Id = 1, Name = "John" },
    new Person { Id = 2, Name = "Jane" },
    new Person { Id = 3, Name = "Alice" }
};

var addresses = new List<Address>
{
    new Address { Id = 1, City = "New York" },
    new Address { Id = 2, City = "London" },
    new Address { Id = 3, City = "Paris" }
};

var joinedData = persons.Join(addresses,
                              person => person.Id,
                              address => address.Id,
                              (person, address) => new { PersonName = person.Name, City = address.City });

Java

List<Person> persons = List.of(
        new Person(1, "John"),
        new Person(2, "Jane"),
        new Person(3, "Alice")
);

List<Address> addresses = List.of(
        new Address(1, "New York"),
        new Address(2, "London"),
        new Address(3, "Paris")
);

List<JoinedData> joinedData = persons.stream()
                                     .join(addresses.stream(),
                                           person -> person.getId(),
                                           address -> address.getId(),
                                           (person, address) -> new JoinedData(person.getName(), address.getCity()))
                                     .collect(Collectors.toList());

Output

[{ PersonName = "John", City = "New York" },
 { PersonName = "Jane", City = "London" },
 { PersonName = "Alice", City = "Paris" }]

Any / AnyMatch

Checks if any element in the sequence satisfies a specified condition.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var anyEven = numbers.Any(n => n % 2 == 0);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean anyEven = numbers.stream()
                         .anyMatch(n -> n % 2 == 0);

Output

true

All / AllMatch

Checks if all elements in the sequence satisfy a specified condition.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var allPositive = numbers.All(n => n > 0);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

boolean allPositive = numbers.stream()
                             .allMatch(n -> n > 0);

Output

true

Contains / contains

Checks if a sequence contains a specific element.

C#

var fruits = new List<string> { "apple", "banana", "cherry" };
var containsBanana = fruits.Contains("banana");

Java

List<String> fruits = Arrays.asList("apple", "banana", "cherry");
boolean containsBanana = fruits.contains("banana");

Output

true

Count / count

Returns the number of elements in a sequence.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var count = numbers.Count();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream()
                    .count();

Output

5

First / findFirst

Returns the first element from a sequence, or throws an exception if the sequence is empty.

C#

var = new List<int> { 1, 2, 3, 4, 5 };
var firstNumber = numbers.First();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int firstNumber = numbers.stream()
                         .findFirst()
                         .orElseThrow(NoSuchElementException::new);

Output

1

FirstOrDefault / findFirst

Returns the first element from a sequence, or returns the default value for the element type if the sequence is empty.

C#

var numbers = new List<int>();
var firstOrDefaultNumber = numbers.FirstOrDefault();

Java

List<Integer> numbers = new ArrayList<>();
int firstOrDefaultNumber = numbers.stream()
                                  .findFirst()
                                  .orElse(0);

Output

0

Last / reduce

Returns the last element from a sequence, or throws an exception if the sequence is empty.

C#

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

int lastNumber = numbers.Last();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int lastNumber = numbers.stream()
                .reduce((a, b) -> b)
                .orElseThrow(NoSuchElementException::new);

Output

5

LastOrDefault / reduce

Returns the last element from a sequence, or returns the default value for the element type if the sequence is empty.

C#

var = new List<int>();
var lastOrDefaultNumber = numbers.LastOrDefault();

Java

List<Integer> numbers = new ArrayList<>();
int lastOrDefaultNumber = numbers.stream()
                                 .reduce((a, b) -> b)
                                 .orElse(0);

Output

0

Skip / skip

Skips a specified number of elements from the start of a sequence.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var skippedNumbers = numbers.Skip(2);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> skippedNumbers = numbers.stream()
                                      .skip(2)
                                      .collect(Collectors.toList());

Output

[3, 4, 5]

SkipWhile / dropWhile

Skips elements in a sequence until a specified condition is no longer satisfied, and includes all the remaining elements.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var skippedNumbers = numbers.SkipWhile(n => n < 3);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> skippedNumbers = numbers.stream()
                                      .dropWhile(n -> n < 3)
                                      .collect(Collectors.toList());

Output

[3, 4, 5]

Take / limit

Returns a specified number of contiguous elements from the start of a sequence.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var takenNumbers = numbers.Take(3);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> takenNumbers = numbers.stream()
                                    .limit(3)
                                    .collect(Collectors.toList());

Output

[1, 2, 3]

TakeWhile / takeWhile

takes elements from a sequence as long as a specified condition is true.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var takenNumbers = numbers.TakeWhile(n => n < 4);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> takenNumbers = numbers.stream()
                                    .takeWhile(n -> n < 4)
                                    .collect(Collectors.toList());

Output

[1, 2, 3]

Sum / Sum

Calculates the sum of a sequence of numeric values.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var sum = numbers.Sum();

Java

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int sum = numbers.Sum();

Output

15

Average / average

Calculates the average of a sequence of numeric values.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var average = numbers.Average();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream()
                        .mapToInt(Integer::intValue)
                        .average()
                        .orElseThrow(NoSuchElementException::new);

Output

3

Min / min

Returns the minimum value from a sequence of comparable elements.

C#

var numbers = new List<int> { 5, 2, 9, 1, 7 };
var min = numbers.Min();

Java

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);
int min = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .min()
                 .orElseThrow(NoSuchElementException::new);

Output

1

Max / max

Returns the maximum value from a sequence of comparable elements.

C#

var numbers = new List<int> { 5, 2, 9, 1, 7 };
var max = numbers.Max();

Java

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);
int max = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .max()
                 .orElseThrow(NoSuchElementException::new);

Output

9

Aggregate / reduce

Performs a custom aggregation operation on the elements of a sequence, combining them iteratively based on a specified lambda expression.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var sum = numbers.Aggregate((acc, num) => acc + num);

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .reduce(0, (acc, num) -> acc + num);

Output

15

Distinct / distinct

Returns a sequence with distinct elements.

C#

var numbers = new List<int> { 1, 2, 2, 3, 3, 4, 5 };
var distinctNumbers = numbers.Distinct();

Java

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
                                       .distinct()
                                       .collect(Collectors.toList());

Output

[1, 2, 3, 4, 5]

Union / concat

Combines two sequences, returning a sequence that contains distinct elements from both sequences.

C#

var numbers1 = new List<int> { 1, 2, 3 };
var numbers2 = new List<int> { 3, 4, 5 };
var unionNumbers = numbers1.Union(numbers2);

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4, 5);
List<Integer> unionNumbers = Stream.concat(numbers1.stream(), numbers2.stream())
                                   .distinct()
                                   .collect(Collectors.toList());

Output

[1, 2, 3, 4, 5]

Intersect / filter

Returns the common elements between two sequences.

C#

var numbers1 = new List<int> { 1, 2, 3, 4 };
var numbers2 = new List<int> { 3, 4, 5 };
var intersectNumbers = numbers1.Intersect(numbers2);

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4);
List<Integer> numbers2 = Arrays.asList(3, 4, 5);
List<Integer> intersectNumbers = numbers1.stream()
                                         .filter(numbers2::contains)
                                         .collect(Collectors.toList());

Output

[3, 4]

Except / filter

Returns the elements from the first sequence that are not present in the second sequence.

C#

var numbers1 = new List<int> { 1, 2, 3, 4 };
var numbers2 = new List<int> { 3, 4, 5 };
var exceptNumbers = numbers1.Except(numbers2);

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4);
List<Integer> numbers2 = Arrays.asList(3, 4, 5);
List<Integer> exceptNumbers = numbers1.stream()
                                      .filter(num -> !numbers2.contains(num))
                                      .collect(Collectors.toList());

Output

[1, 2]

Concat / concat

Concatenates two sequences into a single sequence.

C#

var numbers1 = new List<int> { 1, 2, 3 };
var numbers2 = new List<int> { 3, 4, 5 };
var concatenatedNumbers = numbers1.Concat(numbers2).ToList();

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4, 5);
List<Integer> concatenatedNumbers = Stream.concat(numbers1.stream(), numbers2.stream())
                                          .collect(Collectors.toList());

Output

[1, 2, 3, 3, 4, 5]

Reverse / reverse

Reverses the order of the elements in a sequence.

C#

var numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.Reverse();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Collections.reverse(numbers);

Output

[5, 4, 3, 2, 1]

Zip / mapToObj

Combines two sequences by merging corresponding elements into a new sequence of tuples.

C#

var numbers1 = new List<int> { 1, 2, 3, 4 };
var numbers2 = new List<int> { 10, 20, 30 };

var zippedNumbers = numbers1.Zip(numbers2, (x, y) => x + y).ToList();

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4);
List<Integer> numbers2 = Arrays.asList(10, 20, 30);
List<Integer> zippedNumbers = IntStream.range(0, Math.min(numbers1.size(), numbers2.size()))
                                       .mapToObj(i -> numbers1.get(i) + numbers2.get(i))
                                       .collect(Collectors.toList());

Output

[11, 22, 33]

ElementAt / get

Returns the element at a specified index in a sequence.

C#

var fruits = new List<string> { "apple", "banana", "cherry" };
var element = fruits.ElementAt(1);

Java

List<String> fruits = Arrays.asList("apple", "banana", "cherry");
String element = fruits.get(1);

Output

banana

Single / reduce

Returns a single element from a sequence and throws an exception if the sequence contains zero or more than one element.

C#

var numbers = new List<int> { 1 };
var singleNumber = numbers.Single();

Java

List<Integer> numbers = Arrays.asList(1);
int singleNumber = numbers.stream()
                          .reduce((a, b) -> {
                              throw new IllegalStateException("More than one element found");
                          })
                          .orElseThrow(NoSuchElementException::new);

Output

1

SingleOrDefault / reduce

Returns the single element of a sequence, or the default value if the sequence is empty or contains more than one element.

C#

var numbers = new List<int> { 1, 2, 3 };
var singleOrDefaultNumber = numbers.SingleOrDefault();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3);
int singleOrDefaultNumber = numbers.stream()
                                   .reduce((a, b) -> { 
                                        throw new IllegalStateException("More than one element found");
                                   })
                                   .orElse(0);

Output

0

DefaultIfEmpty

Returns a sequence with a default element if the original sequence is empty.

C#

var numbers = new List<int>();
var numbersWithDefault = numbers.DefaultIfEmpty(5);

Java

List<Integer> numbers = new ArrayList<>();
Iterable<Integer> numbersWithDefault = numbers.isEmpty() 
                                        ? Collections.singletonList(5) 
                                        : numbers;

Output

[5]

SequenceEqual / sequenceEqual

Checks if two sequences are equal by comparing their elements.

C#

var numbers1 = new List<int> { 1, 2, 3 };
var numbers2 = new List<int> { 1, 2, 3 };
var isEqual = numbers1.SequenceEqual(numbers2);

Java

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(1, 2, 3);
boolean isEqual = Objects.equals(numbers1, numbers2);

Output

true

Cast / map (with explicit casting)

Converts the elements of a sequence to a specified type or throws an exception if any element in the sequence cannot be cast to that type.

C#

object[] objects = { 1, 2, 3 };
IEnumerable<int> numbers = objects.Cast<int>();

Java

Object[] objects = { 1, 2, 3 };
List<Integer> numbers = Arrays.stream(objects)
                              .map(obj -> (Integer) obj)
                              .collect(Collectors.toList());

Output

[1, 2, 3]

OfType / filter (with type check)

Filters the elements of a sequence based on a specified type.

C#

var mixedList = new ArrayList { 1, "hello", 3.14, true };
IEnumerable<string> strings = mixedList.OfType<string>();

Java

List<Object> mixedList = Arrays.asList(1, "hello", 3.14, true);
List<String> strings = mixedList.stream()
                                .filter(obj -> obj instanceof String)
                                .map(obj -> (String) obj)
                                .collect(Collectors.toList());

Output

["hello"]

ToArray / toArray

Converts a sequence to an array.

C#

var numbers = new List<int> { 1, 2, 3 };
int[] numberArray = numbers.ToArray();

Java

List<Integer> numbers = Arrays.asList(1, 2, 3);
int[] numberArray = numbers.stream()
                           .mapToInt(Integer::intValue)
                           .toArray();

ToList / asList

Converts a sequence to a list.

C#

int[] numbers = { 1, 2, 3 };
var numberList = numbers.ToList();

Java

int[] numbers = { 1, 2, 3 };
List<Integer> numberList = new ArrayList<>(Arrays.asList(numbers));

ToDictionary / toMap

Converts a sequence to a dictionary based on a specified key and value selector.

C#

var fruits = new List<string> { "apple", "banana", "cherry" };
Dictionary<string, int> fruitLengths = fruits.ToDictionary(fruit => fruit, fruit => fruit.Length);

Java

List<String> fruits = Arrays.asList("apple", "banana", "cherry");
Map<String, Integer> fruitMap = fruits.stream()
                                      .collect(Collectors.toMap(fruit -> fruit, fruit -> fruit.length()));

Output

{
    apple: 5,
    banana: 6,
    cherry: 6
}

ToLookup / groupingBy

Converts a sequence to a lookup based on a specified key selector.

C#

var fruits = new List<string> { "apple", "banana", "cherry", "avocado", "blueberry" };
ILookup<char, string> fruitsByFirstLetter = fruits.ToLookup(fruit => fruit[0]);

Java

List<String> fruits = Arrays.asList("apple", "banana", "cherry", "avocado", "blueberry");
Map<Character, List<String>> fruitsByFirstLetter = fruits.stream()
                                                        .collect(Collectors.groupingBy(fruit -> fruit.charAt(0)));

Output

{
    a = [apple, avocado],
    b = [banana, blueberry],
    c = [cherry]
}

Conclusion

By grasping the mapping of C# LINQ functions to their Java equivalents, we can confidently navigate the transition between these languages.