Wie erhalte ich das umgekehrte Resultat für einen Java Streams Filter?

Java Streams sind eine feine Sache. Ich habe das Gefühl, ich fange mittlerweile alle meine Java Beiträge mit diesem Satz an. 🙂

Ein Java Streams Filter erlaubt es einem, auf einfache Weise alle gewünschten Einträge aus einer Liste herauszufiltern und den Rest wegzuschmeissen. Alle roten Elefanten, alle Kinder unter 6 Jahren, alle Schlangen mit Flügeln und so weiter.

Oftmals ist es aber einfacher zu sagen, was man nicht will. Als quasi ‚Schau her, ich selektiere alle Kinder zwischen 5 und 8 Jahren… und jetzt, du superkluges Java, gib mir alle anderen Kinder!

Wie macht man das?

Not Predicate definieren

Ganz einfach: Wo auch immer man den Filter umgekehren will definiert man ein Not Predicate. Predicate ist ja dieses Dings in dem Filter, und wenn das „true“ ist, wird der jeweilige Eintrag behalten. Und mit einem Not Predicate drehen wir die Logik einfach um.

Folgenden Code irgendwo hinschmeissen:

public static <T> Predicate<T> not(Predicate<T> t) {
    return t.negate();
}

Und dann kann man das not im Filter verwenden.

Beispiel: Ihr habt eine Liste mit Kindernamen und findet den Namen „Rupert“ einfach wiederlich, den wollt Ihr unbedingt losweren. Also definieren wir zuerst einen Filter für den Namen ‚Rupert‘:

Arrays.stream(kindernamen).filter(kindername -> kindername.contains("Rupert")).collect(Collectors.toList());

Dieser Filter würde jetzt also alle Ruperts in der Resultateliste haben.

Dann kehren wir den Filter einfach um:

Arrays.stream(kindernamen).filter(not(kindername -> kindername.contains("Rupert"))).collect(Collectors.toList());

Et voilà.

Java 11 hats schon drin

Kleine Randbemerkung: In Java 11 ist genau diese Methode beim Predicate schon dabei:

Predicate.not( … )

Siehe https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/Predicate.html#not(java.util.function.Predicate)

Dann geht unser Rupert-Hass-Filter folgendermassen (und das Predicate muss man natürlich nicht mehr selber definieren):

Arrays.stream(kindernamen).filter(Predicate.not(kindername -> kindername.contains("Rupert"))).collect(Collectors.toList());
Werbeanzeigen

Wie verwende ich Java Streams bei zwei For Loops?

Streams sind wirklich eine feine Sache. So fein, dass ich heutzutage praktisch keine For-Loops mehr sehe. Streams sind auch tatsächlich einfach und am Ende sauschnell, um die Daten zu verarbeiten.

Aber wie verwende ich Streams, wenn ich zwei For Loops benötigen würde für die Verarbeitung? Also wenn ich eine „Liste in einer anderen Liste“ habe?

Die kurze Antwort ist:

  • Wenn man beim Verarbeiten der inneren Liste auf die Objekte der äusseren Liste zugreifen will, verwendet man zwei Streams() mit AnyMatch.
  • Wenn man beim Verarbeiten der inneren Liste nicht auf die Objekte der äusseren Liste zugreifen will, kann man FlatMap verwenden.

FlatMap? Was ist das schon wieder für eine Teufelei?

Ja schrecklich, nicht? Da denkst Du, Du hättest endlich mal alles wissenswerte über Java gelernt, und dann fange ich an, von FlatMaps zu schwafeln.

Um FlatMaps zu erklären fange ich gleich an mit dem Beispiel, dass ich auch unten im Code zeigen werden:

Ich habe eine Liste von Müttern und jede Mutter hat wiederum eine Liste von Ihren Kindern. Meine Aufgabe ist es, die Mutter mit dem Kind „Paul“ zu finden.

Die Daten könnten also folgendermassen aussehen:

Äussere Liste:

  • Mutter Janet
  • Mutter Petra

Janet hat zwei Kinder:

  • John
  • Jessica

Petra hat zwei Kinder:

  • Patrick
  • Paul

Wenn ich jetzt eine FlatMap verwende, erstelle ich für die Mütter einen Stream und erhalte dann einfach alle Kinder aneinandergereiht statt „pro Mutter“

Also statt

Janet { John, Jessica } Petra { Patrick, Paul }

erhalte ich

{ John, Jessica, Patrick, Paul }

Wenn ich jetzt die Information der Mutter nicht benötige und nur die Attribute eines Kindes prüfen kann (Beispielsweise: „Ist das Kind älter als 6 Jahre?“) dann funktioniert eine FlatMap sehr gut.

Sobald ich aber die Information der dazugehörigen Mutter brauche, geht das nicht mehr (Beispiel: „Zu welcher Mutter gehört das Kind Paul?“). Dann kann ich zwei Streams verwenden (siehe Codebeispiel unten).

Okay ich bin bereit. Bring mir den Code!

Der Code zeigt zuerst, wie man für Paul auf herkömmliche Weise herausfindet, wer seine Mutter ist. Dann verwende ich zwei Streams mit anyMatch im inneren Stream. Und am Ende noch eine FlatMap, wobei ich aber fälschlicherweise am Ende das Kind erhalte statt die Mutter.

Die Klassen für Mutter und Kind sowie das setzen der Werte finden sich ganz unten in der Klasse.

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class FindPaul {

List<Mother> mothers = new ArrayList<>();

public void findPaul() {
	setupMothers();
	twoForLoops();
	twoStreams();
	flatMap();	
}

private void twoForLoops() {
	for(Mother  mother: mothers) {
		for (Child child: mother.children) {
			if("Paul".equals(child.name)) {
				System.out.println("1: " + mother.name + " is the mother of Paul");
			}
		}
	}
}

private void twoStreams() {
	Optional<Mother> paulsMother = mothers.stream().filter(
		mother -> mother.children.stream().anyMatch(child -> "Paul".equals(child.name))				
	).findFirst();				
	System.out.println("2: " + paulsMother.get().name + " is the mother of Paul");		
}

// flatMap makes the conversion: 
// Stream<List<Object>>	-> flatMap ->	Stream<Object>
private void flatMap() {
	// Fehler: Mit Flatmap hat man keinen Bezug mehr auf das "Parent" Element
	// Man kriegt ein "Child", nicht die Mutter
	Optional<Child> paulsMother = mothers.stream().flatMap(mother -> mother.children.stream()).filter(child -> "Paul".equals(child.name)).findFirst();					
	System.out.println("3: " + paulsMother.get().name + " is Paul itself, and who is the mother?");
}

private void setupMothers() {
	Mother m1 = new Mother("Janet");
	m1.children.add(new Child("John"));
	m1.children.add(new Child("Jessica"));
	
	Mother m2 = new Mother("Petra");
	m2.children.add(new Child("Patrick"));
	m2.children.add(new Child("Paul"));
	
	mothers.add(m1);
	mothers.add(m2);
}

class Child {
	String name;		
	Child(String name) { this.name = name; }
}

class Mother {
	String name;
	List<Child> children = new ArrayList<>();
	Mother(String name) { this.name = name; }
}	
}

Übrigens: Falls jemand eine noch bessere Methode hat, um zwei For Loops zu verarbeiten, bin ich jederzeit bereit, etwas dazuzulernen. 🙂

Du kannst in deinem Java 8 Projekt keine JAR Datei verwenden die in einer höheren Version kompiliert wurde

Was wie das elfte Gebot klingt ist mir tatsächlich passiert: Ich habe an meinem Java 8 Projekt gearbeitet und musste dann eine Model-Klasse von einem anderen Projekt verwenden. Doof nur: Das andere Projekt ist bereits in Java 11 programmiert (noch ohne Module zu verwenden).

Ich habe mir nichts dabei gedacht, sondern wie gewohnt die Maven Abhängigkeit im Pom.xml eingetragen und kurz darauf habe ich die Jar-Datei in meinem Projekt in IntelliJ gefunden.

Als ich die Klasse versucht habe, zu verwenden, war IntelliJ sogar noch höflich zu mir und hat mir gesagt „Ja gut, ich kenne diese Klasse, die liegt doch in diesem Jar-Dings, du kannst sie also einfach importieren„.

Als ich die Klasse aber tatsächlich importiert habe kam nur die Meldung „Cannot resolve symbol„.

Ich habe alles mögliche versucht: Den Cache in IntelliJ invalidiert+restartet, den Target Ordner gelöscht und nochmals Maven:clean-install gemacht, und so weiter, aber nichts hat geholfen. IntelliJ konnte die Klasse einfach nicht finden. Selbst im Classpath war die Klasse beziehungsweise das Jar vorhanden.

Ganz zufällig habe ich etwas entdeckt, als ich die Klasse dekompiliert betrachtet habe: IntelliJ hat mich darauf hingewiesen, dass die Klasse in Java 11 kompiliert wurde. Das habe ich zu Beginn nicht gewusst, obwohl ich in diesem Blogpost natürlich explizit über genau diese Situation schreibe.

Was auch mühsam war: Bei meiner Internetrecherche bin ich auf niemanden getroffen, der die gleiche Situation beschrieben hat. Sobald man irgendwas wie „Use class from java 11 in java 8 project“ bei Google sucht kommen alles nur Anleitungen, wie man von Java 8 auf Java 11 migriert.

Weder die Internetrecherche noch meine Arbeitskollegen konnten mir weiterhelfen. Was also tun? Natürlich! Frage auf Stackoverflow gepostet und prompt hatte ich 8 Minuten später ganze 3 Antworten, die es mir erklärten:

You cannot use a jar file compiled in higher version of java in a project developed in lower version of java

Da momentan viele Firmen noch Java 8 verwenden und gegebenfalls zum Teil bereits auf Java 11 gewechselt haben ist es sicherlich praktisch, wenn jeder Java-Entwickler diesen Satz zumindest mal gehört hat.