สวัสดี! ในภารกิจ Java Syntax Proเราได้ศึกษานิพจน์แลมบ์ดาและกล่าวว่าสิ่งเหล่านี้ไม่ได้มีอะไรมากไปกว่าการนำวิธีการทำงานไปใช้จากอินเทอร์เฟซการทำงาน กล่าวอีกนัยหนึ่ง นี่คือการนำคลาสที่ไม่ระบุชื่อ (ไม่ทราบ) ไปใช้ ซึ่งเป็นวิธีการที่ยังไม่เกิดขึ้นจริง และหากในการบรรยายของหลักสูตรเราได้เจาะลึกถึงการจัดการกับนิพจน์แลมบ์ดาตอนนี้เราจะพิจารณาอีกด้านหนึ่ง: กล่าวคืออินเทอร์เฟซเหล่านี้เอง
Java เวอร์ชันที่แปดนำเสนอแนวคิดของ อินเท อร์เฟซการทำงาน นี่คืออะไร? อินเทอร์เฟซที่มีวิธีหนึ่งที่ไม่ได้ใช้งาน (นามธรรม) ถือว่าใช้งานได้ อินเทอร์เฟซแบบสำเร็จรูปจำนวนมากอยู่ภายใต้คำจำกัดความนี้ เช่น อินเทอร์เฟซที่กล่าวถึงก่อนหน้า
ภาคแสดง
ผู้บริโภค
ผู้จัดหา
การทำงาน
UnaryOperator
และมีหลายวิธีที่
นั่นคือทั้งหมด! คงจะดีมากถ้าหลังจากอ่านบทความนี้แล้ว คุณเข้าใกล้ความเข้าใจและการเรียนรู้ Stream API ใน Java มากขึ้นอีกก้าวหนึ่ง!
Comparator
นี้ และยังมีอินเทอร์เฟซที่เราสร้างขึ้นเอง เช่น:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
เรามีอินเทอร์เฟซที่มีหน้าที่แปลงวัตถุประเภทหนึ่งให้เป็นวัตถุประเภทอื่น (อะแดปเตอร์ชนิดหนึ่ง) คำอธิบายประกอบ@FunctionalInterface
ไม่ใช่สิ่งที่ซับซ้อนหรือสำคัญอย่างยิ่ง เนื่องจากมีวัตถุประสงค์เพื่อบอกคอมไพเลอร์ว่าอินเทอร์เฟซที่กำหนดนั้นใช้งานได้และไม่ควรมีมากกว่าหนึ่งวิธี หากอินเทอร์เฟซที่มีคำอธิบายประกอบนี้มีวิธีการที่ไม่ได้ใช้งาน (นามธรรม) มากกว่าหนึ่งวิธี คอมไพเลอร์จะไม่ข้ามอินเทอร์เฟซนี้ เนื่องจากจะรับรู้ว่าเป็นโค้ดที่ผิดพลาด อินเทอร์เฟซที่ไม่มีคำอธิบายประกอบนี้ถือว่าใช้งานได้และจะใช้งานได้ แต่ @FunctionalInterface
นี่ก็ไม่มีอะไรมากไปกว่าการประกันเพิ่มเติม กลับไปที่ชั้นเรียนกันComparator
เถอะ หากคุณดูที่โค้ด (หรือเอกสารประกอบ ) คุณจะเห็นว่ามันมีมากกว่าหนึ่งวิธี ถ้าอย่างนั้นคุณถามว่า: แล้วจะถือเป็นอินเทอร์เฟซที่ใช้งานได้ได้อย่างไร? อินเทอร์เฟซแบบนามธรรมสามารถมีวิธีการที่ไม่อยู่ในขอบเขตของวิธีการเดียวได้:
- คงที่
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
หลังจากได้รับวิธีนี้ คอมไพเลอร์ไม่ได้บ่น ซึ่งหมายความว่าอินเทอร์เฟซของเรายังคงใช้งานได้
- วิธีการเริ่มต้น
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
ขอย้ำอีกครั้งว่าคอมไพลเลอร์ไม่ได้เริ่มบ่น และเราไม่ได้ก้าวข้ามข้อจำกัดของอินเทอร์เฟซการทำงาน
- วิธีการเรียนวัตถุ
Object
เราได้พูดถึงความ จริงที่ว่าคลาสทั้งหมดสืบทอดมาจากคลาส สิ่งนี้ใช้ไม่ได้กับอินเทอร์เฟซ แต่ถ้าเรามีวิธีนามธรรมในอินเทอร์เฟซที่ตรงกับลายเซ็นกับวิธีการบางอย่างของคลาสObject
วิธีการดังกล่าว (หรือวิธีการ) จะไม่ทำลายข้อจำกัดอินเทอร์เฟซการทำงานของเรา:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
และขอย้ำอีกครั้งว่าคอมไพเลอร์ของเราไม่บ่น ดังนั้นอินเทอร์เฟซConverter
จึงยังถือว่าใช้งานได้ ตอนนี้คำถามคือ: เหตุใดเราจึงต้องจำกัดตัวเองให้เหลือเพียงวิธีที่ไม่ได้ใช้งานเพียงวิธีเดียวในอินเทอร์เฟซการทำงาน? จากนั้นเพื่อให้เราสามารถนำไปใช้งานโดยใช้แลมบ์ดา ลองดูตัวอย่างนี้Converter
ด้วย เพื่อที่จะทำสิ่งนี้ เรามาสร้างคลาสกันDog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
และอันที่คล้ายกันRaccoon
(แรคคูน):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
สมมติว่าเรามีวัตถุDog
และเราจำเป็นต้องสร้างวัตถุตามฟิลด์ของRaccoon
มัน นั่นคือConverter
จะแปลงวัตถุประเภทหนึ่งไปเป็นอีกประเภทหนึ่ง จะมีลักษณะอย่างไร:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
เมื่อเรารันมัน เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
และนั่นหมายความว่าวิธีการของเราทำงานได้อย่างถูกต้อง
อินเทอร์เฟซการทำงานพื้นฐานของ Java 8
ตอนนี้เรามาดูอินเทอร์เฟซการทำงานหลายอย่างที่ Java 8 นำมาให้เราและใช้งานร่วมกับ Stream API อย่างจริงจังภาคแสดง
Predicate
— อินเทอร์เฟซการทำงานสำหรับการตรวจสอบว่าตรงตามเงื่อนไขที่กำหนดหรือไม่ หากตรงตามเงื่อนไข ให้ส่งคืนtrue
มิฉะนั้น - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
เป็นตัวอย่าง ให้ลองสร้าง a Predicate
ที่จะตรวจสอบความเท่าเทียมกันของจำนวนประเภทInteger
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
เอาต์พุตคอนโซล:
true
false
ผู้บริโภค
Consumer
(จากภาษาอังกฤษ - "ผู้บริโภค") - อินเทอร์เฟซการทำงานที่รับออบเจ็กต์ประเภท T เป็นอาร์กิวเมนต์อินพุต ดำเนินการบางอย่าง แต่ไม่ส่งคืนสิ่งใด:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
ตามตัวอย่าง ให้พิจารณาซึ่งมีหน้าที่ส่งออกคำทักทายไปยังคอนโซลด้วยอาร์กิวเมนต์สตริงที่ส่ง: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
เอาต์พุตคอนโซล:
Hello Elena !!!
ผู้จัดหา
Supplier
(จากภาษาอังกฤษ - ผู้ให้บริการ) - อินเทอร์เฟซการทำงานที่ไม่ได้รับอาร์กิวเมนต์ใด ๆ แต่ส่งคืนออบเจ็กต์ประเภท T:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
เป็นตัวอย่าง พิจารณาSupplier
ซึ่งจะสร้างชื่อแบบสุ่มจากรายการ:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
และถ้าเรารันสิ่งนี้ เราจะเห็นผลลัพธ์แบบสุ่มจากรายชื่อในคอนโซล
การทำงาน
Function
— อินเทอร์เฟซการทำงานนี้รับอาร์กิวเมนต์ T และส่งไปยังวัตถุประเภท R ซึ่งจะถูกส่งกลับเป็นผล:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
ตัวอย่างเช่น ลองใช้ซึ่งแปลงตัวเลขจากรูปแบบสตริง ( ) เป็นรูปแบบตัวเลข ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
เมื่อเรารันมัน เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:
678
PS: หากเราไม่เพียงส่งผ่านตัวเลขเท่านั้น แต่ยังรวมถึงอักขระ อื่น ๆ ลงในสตริงด้วยข้อยกเว้น จะถูกส่งออกไป -NumberFormatException
UnaryOperator
UnaryOperator
— อินเทอร์เฟซการทำงานที่รับวัตถุประเภท T เป็นพารามิเตอร์ ดำเนินการบางอย่างกับมัน และส่งกลับผลลัพธ์ของการดำเนินการในรูปแบบของวัตถุประเภทเดียวกัน T:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
ซึ่งใช้วิธีการapply
ในการยกกำลังสองตัวเลข:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
เอาต์พุตคอนโซล:
81
เราดูอินเทอร์เฟซการทำงานห้าแบบ นี่ไม่ใช่ทั้งหมดที่เราสามารถใช้ได้ตั้งแต่ Java 8 - นี่คืออินเทอร์เฟซหลัก สิ่งที่เหลืออยู่คืออะนาล็อกที่ซับซ้อน รายการทั้งหมดสามารถพบได้ในเอกสารอย่างเป็นทางการของ Oracle
อินเทอร์เฟซการทำงานในสตรีม
ตามที่กล่าวไว้ข้างต้น อินเทอร์เฟซการทำงานเหล่านี้เชื่อมโยงอย่างแน่นหนากับ Stream API คุณถามอย่างไร?
Stream
ทำงานโดยเฉพาะกับอินเทอร์เฟซการทำงานเหล่านี้ มาดูกันว่าอินเทอร์เฟซการทำงานสามารถใช้งานได้อย่างไรในStream
.
วิธีการที่มีภาคแสดง
ตัวอย่างเช่น ลองใช้วิธีการเรียนStream
ซึ่งfilter
ใช้เป็นอาร์กิวเมนต์Predicate
และส่งกลับ เฉพาะองค์ประกอบที่ตรง ตามStream
เงื่อนไข Predicate
ในบริบทของStream
-a หมายความว่าจะส่งผ่านเฉพาะองค์ประกอบที่ส่งคืนtrue
เมื่อใช้ใน วิธี test
อินเทอร์เฟซPredicate
เท่านั้น นี่คือลักษณะที่ตัวอย่างของเราจะมีลักษณะสำหรับPredicate
แต่สำหรับตัวกรององค์ประกอบในStream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
ด้วยเหตุนี้ รายการevenNumbers
จะประกอบด้วยองค์ประกอบ {2, 4, 6, 8} และอย่างที่เราจำได้collect
มันจะรวบรวมองค์ประกอบทั้งหมดไว้ในคอลเล็กชั่นหนึ่ง: ในกรณีของเรา เข้าไปในList
.
วิธีการกับผู้บริโภค
หนึ่งในวิธีการในStream
ซึ่งใช้อินเทอร์เฟซการทำงานConsumer
คือpeek
. นี่คือสิ่งที่ตัวอย่างของเราในConsumer
in จะมีลักษณะดังนี้ Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
เอาต์พุตคอนโซล:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
แต่เนื่องจากวิธีการนี้peek
ใช้ได้กับConsumer
การแก้ไขสตริงในStream
จะไม่เกิดขึ้น แต่peek
จะกลับมาStream
พร้อมกับองค์ประกอบดั้งเดิม: เช่นเดียวกับที่เกิดขึ้น ดังนั้นรายการpeopleGreetings
จะประกอบด้วยองค์ประกอบ "Elena", "John", "Alex", "Jim", "Sara" นอกจากนี้ยังมีวิธีการที่ใช้กันทั่วไปforeach
ซึ่งคล้ายกับวิธีการpeek
แต่ความแตกต่างคือเป็นขั้นตอนสุดท้าย
วิธีการกับซัพพลายเออร์
ตัวอย่างของวิธีการStream
ที่ใช้อินเทอร์เฟซการทำงานSupplier
คือgenerate
ซึ่งสร้างลำดับที่ไม่สิ้นสุดขึ้นอยู่กับอินเทอร์เฟซการทำงานที่ส่งผ่านไป ลองใช้ตัวอย่างของเราSupplier
เพื่อพิมพ์ชื่อสุ่มห้าชื่อไปยังคอนโซล:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
และนี่คือผลลัพธ์ที่เราได้รับในคอนโซล:
John
Elena
Elena
Elena
Jim
ที่นี่เราใช้วิธีนี้limit(5)
เพื่อกำหนดขีดจำกัดของวิธีการgenerate
มิฉะนั้นโปรแกรมจะพิมพ์ชื่อแบบสุ่มไปยังคอนโซลอย่างไม่มีกำหนด
วิธีการที่มีฟังก์ชัน
ตัวอย่างทั่วไปของวิธีการที่มีStream
การโต้แย้งFunction
คือวิธีmap
ที่รับองค์ประกอบประเภทหนึ่ง ทำบางอย่างกับองค์ประกอบเหล่านั้น และส่งต่อ แต่สิ่งเหล่านี้อาจเป็นองค์ประกอบของประเภทอื่นอยู่แล้ว ตัวอย่างที่มีFunction
in อาจมีลักษณะดังนี้ Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
เป็นผลให้เราได้รับรายการตัวเลข แต่ในรูปแบบInteger
.
วิธีการด้วย UnaryOperator
ในฐานะที่เป็นวิธีการที่ใช้เป็นUnaryOperator
อาร์กิวเมนต์ ลองใช้วิธีการเรียนStream
- iterate
วิธีการนี้คล้ายกับวิธีการgenerate
: มันยังสร้างลำดับอนันต์แต่มีสองอาร์กิวเมนต์:
- ประการแรกคือองค์ประกอบที่การสร้างลำดับเริ่มต้นขึ้น
- อันที่สองคือ
UnaryOperator
ซึ่งบ่งบอกถึงหลักการของการสร้างองค์ประกอบใหม่จากองค์ประกอบแรก
UnaryOperator
แต่ในวิธีการiterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
เมื่อเรารันมัน เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:
9
81
6561
43046721
นั่นคือแต่ละองค์ประกอบของเราจะถูกคูณด้วยตัวมันเอง และต่อไปเรื่อยๆ สำหรับตัวเลขสี่ตัวแรก 
GO TO FULL VERSION