JavaRush /จาวาบล็อก /Random-TH /อินเทอร์เฟซการทำงานใน Java

อินเทอร์เฟซการทำงานใน Java

เผยแพร่ในกลุ่ม
สวัสดี! ในภารกิจ Java Syntax Proเราได้ศึกษานิพจน์แลมบ์ดาและกล่าวว่าสิ่งเหล่านี้ไม่ได้มีอะไรมากไปกว่าการนำวิธีการทำงานไปใช้จากอินเทอร์เฟซการทำงาน กล่าวอีกนัยหนึ่ง นี่คือการนำคลาสที่ไม่ระบุชื่อ (ไม่ทราบ) ไปใช้ ซึ่งเป็นวิธีการที่ยังไม่เกิดขึ้นจริง และหากในการบรรยายของหลักสูตรเราได้เจาะลึกถึงการจัดการกับนิพจน์แลมบ์ดาตอนนี้เราจะพิจารณาอีกด้านหนึ่ง: กล่าวคืออินเทอร์เฟซเหล่านี้เอง อินเทอร์เฟซการทำงานใน Java - 1Java เวอร์ชันที่แปดนำเสนอแนวคิดของ อินเท อร์เฟซการทำงาน นี่คืออะไร? อินเทอร์เฟซที่มีวิธีหนึ่งที่ไม่ได้ใช้งาน (นามธรรม) ถือว่าใช้งานได้ อินเทอร์เฟซแบบสำเร็จรูปจำนวนมากอยู่ภายใต้คำจำกัดความนี้ เช่น อินเทอร์เฟซที่กล่าวถึงก่อนหน้าComparatorนี้ และยังมีอินเทอร์เฟซที่เราสร้างขึ้นเอง เช่น:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
เรามีอินเทอร์เฟซที่มีหน้าที่แปลงวัตถุประเภทหนึ่งให้เป็นวัตถุประเภทอื่น (อะแดปเตอร์ชนิดหนึ่ง) คำอธิบายประกอบ@FunctionalInterfaceไม่ใช่สิ่งที่ซับซ้อนหรือสำคัญอย่างยิ่ง เนื่องจากมีวัตถุประสงค์เพื่อบอกคอมไพเลอร์ว่าอินเทอร์เฟซที่กำหนดนั้นใช้งานได้และไม่ควรมีมากกว่าหนึ่งวิธี หากอินเทอร์เฟซที่มีคำอธิบายประกอบนี้มีวิธีการที่ไม่ได้ใช้งาน (นามธรรม) มากกว่าหนึ่งวิธี คอมไพเลอร์จะไม่ข้ามอินเทอร์เฟซนี้ เนื่องจากจะรับรู้ว่าเป็นโค้ดที่ผิดพลาด อินเทอร์เฟซที่ไม่มีคำอธิบายประกอบนี้ถือว่าใช้งานได้และจะใช้งานได้ แต่ @FunctionalInterfaceนี่ก็ไม่มีอะไรมากไปกว่าการประกันเพิ่มเติม กลับไปที่ชั้นเรียนกันComparatorเถอะ หากคุณดูที่โค้ด (หรือเอกสารประกอบ ) คุณจะเห็นว่ามันมีมากกว่าหนึ่งวิธี ถ้าอย่างนั้นคุณถามว่า: แล้วจะถือเป็นอินเทอร์เฟซที่ใช้งานได้ได้อย่างไร? อินเทอร์เฟซแบบนามธรรมสามารถมีวิธีการที่ไม่อยู่ในขอบเขตของวิธีการเดียวได้:
  • คงที่
แนวคิดของอินเทอร์เฟซบอกเป็นนัยว่าหน่วยของโค้ดที่กำหนดไม่สามารถใช้วิธีการใด ๆ ได้ แต่เริ่มต้นด้วย Java 8 มันเป็นไปได้ที่จะใช้วิธีการคงที่และค่าเริ่มต้นในอินเทอร์เฟซ วิธีการคงที่ถูกผูกไว้กับคลาสโดยตรงและไม่จำเป็นต้องมีวัตถุเฉพาะของคลาสนั้นเพื่อเรียกวิธีการดังกล่าว นั่นคือวิธีการเหล่านี้เข้ากันได้อย่างลงตัวกับแนวคิดของอินเทอร์เฟซ ตัวอย่างเช่น เรามาเพิ่มวิธีการคงที่สำหรับการตรวจสอบวัตถุที่เป็นโมฆะในคลาสก่อนหน้า:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
หลังจากได้รับวิธีนี้ คอมไพเลอร์ไม่ได้บ่น ซึ่งหมายความว่าอินเทอร์เฟซของเรายังคงใช้งานได้
  • วิธีการเริ่มต้น
ก่อน Java 8 หากเราต้องการสร้างเมธอดในอินเทอร์เฟซที่สืบทอดมาจากคลาสอื่น เราสามารถสร้างได้เฉพาะเมธอดนามธรรมที่ถูกนำไปใช้ในแต่ละคลาสเฉพาะเท่านั้น แต่จะเกิดอะไรขึ้นถ้าวิธีนี้เหมือนกันสำหรับทุกคลาส? ในกรณีนี้ คลาสนามธรรมถูกใช้บ่อยที่สุด แต่เริ่มต้นด้วย Java 8 มีตัวเลือกให้ใช้อินเทอร์เฟซกับวิธีการที่นำไปใช้ - วิธีการเริ่มต้น เมื่อสืบทอดอินเทอร์เฟซ คุณสามารถแทนที่วิธีการเหล่านี้หรือปล่อยให้ทุกอย่างเหมือนเดิม (ปล่อยให้ตรรกะเริ่มต้น) เมื่อสร้างวิธีการเริ่มต้น เราต้องเพิ่มคำหลัก - 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());
   }
}
ขอย้ำอีกครั้งว่าคอมไพลเลอร์ไม่ได้เริ่มบ่น และเราไม่ได้ก้าวข้ามข้อจำกัดของอินเทอร์เฟซการทำงาน
  • วิธีการเรียนวัตถุ
ในการบรรยายเรื่อง Comparing ObjectsObject เราได้พูดถึงความ จริงที่ว่าคลาสทั้งหมดสืบทอดมาจากคลาส สิ่งนี้ใช้ไม่ได้กับอินเทอร์เฟซ แต่ถ้าเรามีวิธีนามธรรมในอินเทอร์เฟซที่ตรงกับลายเซ็นกับวิธีการบางอย่างของคลาส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 - 2

อินเทอร์เฟซการทำงานพื้นฐานของ 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"));
}
เมื่อเรารันมัน เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:

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 คุณถามอย่างไร? อินเทอร์เฟซการทำงานใน Java - 3และมีหลายวิธีที่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
นั่นคือแต่ละองค์ประกอบของเราจะถูกคูณด้วยตัวมันเอง และต่อไปเรื่อยๆ สำหรับตัวเลขสี่ตัวแรก อินเทอร์เฟซการทำงานใน Java - 4นั่นคือทั้งหมด! คงจะดีมากถ้าหลังจากอ่านบทความนี้แล้ว คุณเข้าใกล้ความเข้าใจและการเรียนรู้ Stream API ใน Java มากขึ้นอีกก้าวหนึ่ง!
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION