สวัสดี! ในภารกิจ 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);
} ตัวอย่างเช่น ลองใช้ซึ่งแปลงตัวเลขจากรูปแบบสตริง ( ) เป็นรูปแบบตัวเลข ( ): FunctionStringInteger
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
} เมื่อเรารันมัน เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:
678PS: หากเราไม่เพียงส่งผ่านตัวเลขเท่านั้น แต่ยังรวมถึงอักขระ อื่น ๆ ลงในสตริงด้วยข้อยกเว้น จะถูกส่งออกไป -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. นี่คือสิ่งที่ตัวอย่างของเราในConsumerin จะมีลักษณะดังนี้ 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ที่รับองค์ประกอบประเภทหนึ่ง ทำบางอย่างกับองค์ประกอบเหล่านั้น และส่งต่อ แต่สิ่งเหล่านี้อาจเป็นองค์ประกอบของประเภทอื่นอยู่แล้ว ตัวอย่างที่มีFunctionin อาจมีลักษณะดังนี้ 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 นั่นคือแต่ละองค์ประกอบของเราจะถูกคูณด้วยตัวมันเอง และต่อไปเรื่อยๆ สำหรับตัวเลขสี่ตัวแรก
นั่นคือทั้งหมด! คงจะดีมากถ้าหลังจากอ่านบทความนี้แล้ว คุณเข้าใกล้ความเข้าใจและการเรียนรู้ Stream API ใน Java มากขึ้นอีกก้าวหนึ่ง!
GO TO FULL VERSION