JavaRush /وبلاگ جاوا /Random-FA /رابط های کاربردی در جاوا

رابط های کاربردی در جاوا

در گروه منتشر شد
سلام! در جستجوی Java Syntax Pro، ما عبارات لامبدا را مورد مطالعه قرار دادیم و گفتیم که آنها چیزی بیش از اجرای یک روش تابعی از یک رابط کاربردی نیستند. به عبارت دیگر، این پیاده سازی یک کلاس ناشناس (ناشناخته) است، روش تحقق نیافته آن. و اگر در سخنرانی های دوره به دستکاری با عبارات لامبدا پرداختیم، اکنون به اصطلاح، طرف دیگر را در نظر خواهیم گرفت: یعنی همین رابط ها. رابط های کاربردی در جاوا - 1نسخه هشتم جاوا مفهوم رابط های کاربردی را معرفی کرد . این چیه؟ یک رابط با یک روش اجرا نشده (انتزاعی) کاربردی در نظر گرفته می شود. بسیاری از رابط‌های خارج از جعبه تحت این تعریف قرار می‌گیرند، مانند رابطی که قبلاً مورد بحث قرار گرفت Comparator. و همچنین رابط هایی که خودمان ایجاد می کنیم، مانند:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
ما یک رابط داریم که وظیفه آن تبدیل اشیاء از یک نوع به اشیاء نوع دیگر (نوعی آداپتور) است. حاشیه نویسی @FunctionalInterfaceچیز فوق العاده پیچیده یا مهمی نیست، زیرا هدف آن این است که به کامپایلر بگوید که این رابط کاربردی است و نباید بیش از یک روش داشته باشد. اگر یک رابط با این حاشیه نویسی بیش از یک روش اجرا نشده (انتزاعی) داشته باشد، کامپایلر از این رابط عبور نمی کند، زیرا آن را به عنوان کد اشتباه درک می کند. رابط های بدون این حاشیه نویسی را می توان کاربردی در نظر گرفت و کار خواهد کرد، اما @FunctionalInterfaceاین چیزی بیش از بیمه اضافی نیست. برگردیم سر Comparatorکلاس اگر به کد (یا مستندات ) آن نگاه کنید ، می‌بینید که بیش از یک روش دارد. سپس می‌پرسید: پس چگونه می‌توان آن را یک رابط کاربردی در نظر گرفت؟ رابط های انتزاعی می توانند متدهایی داشته باشند که در محدوده یک روش واحد نیستند:
  • ایستا
مفهوم واسط نشان می دهد که یک واحد کد معین نمی تواند هیچ روشی را پیاده سازی کند. اما با شروع جاوا 8، امکان استفاده از روش های استاتیک و پیش فرض در رابط ها فراهم شد. متدهای استاتیک مستقیماً به یک کلاس متصل می شوند و برای فراخوانی چنین متدی به شی خاصی از آن کلاس نیاز ندارند. یعنی این روش ها به طور هماهنگ در مفهوم رابط ها قرار می گیرند. به عنوان مثال، بیایید یک متد استاتیک برای بررسی یک شی برای null به کلاس قبلی اضافه کنیم:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
پس از دریافت این روش، کامپایلر شکایتی نکرد، به این معنی که رابط ما هنوز کار می کند.
  • روش های پیش فرض
قبل از جاوا 8، اگر ما نیاز به ایجاد متدی در یک رابط داشتیم که توسط کلاس های دیگر به ارث برده شده بود، فقط می توانستیم یک متد انتزاعی ایجاد کنیم که در هر کلاس خاص پیاده سازی شده بود. اما اگر این روش برای همه کلاس ها یکسان باشد چه؟ در این مورد ، کلاس های انتزاعی بیشتر مورد استفاده قرار می گرفتند . اما با شروع با جاوا 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());
   }
}
باز هم می بینیم که کامپایلر شروع به شکایت نکرد و از محدودیت های رابط عملکردی فراتر نرفته ایم.
  • متدهای کلاس شی
در سخنرانی مقایسه اشیا در مورد این واقعیت صحبت کردیم که همه کلاس ها از کلاس ارث می برند 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
و این بدان معنی است که روش ما به درستی کار کرده است.رابط های کاربردی در جاوا - 2

رابط های کاربردی پایه جاوا 8

خوب، اکنون اجازه دهید به چندین رابط کاربردی که جاوا 8 برای ما آورده است و به طور فعال در ارتباط با Stream API استفاده می شود، نگاه کنیم.

محمول

Predicate- یک رابط کاربردی برای بررسی اینکه آیا یک شرط خاص برآورده شده است یا خیر. اگر شرط برآورده شود، برمی گردد true، در غیر این صورت - false:
@FunctionalInterface
public interface Predicate<T> {
   boolean test(T t);
}
به عنوان مثال، ایجاد یک را در نظر بگیرید 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
ما به پنج رابط کاربردی نگاه کردیم. این تنها چیزی نیست که با جاوا 8 در دسترس ما است - اینها رابط های اصلی هستند. بقیه موارد موجود آنالوگ های پیچیده آنها هستند. لیست کامل را می توان در اسناد رسمی Oracle یافت .

رابط های کاربردی در Stream

همانطور که در بالا توضیح داده شد، این رابط های کاربردی به شدت با Stream API همراه هستند. میپرسی چطور؟ رابط های کاربردی در جاوا - 3و به طوری که بسیاری از روش ها Streamبه طور خاص با این رابط های کاربردی کار می کنند. بیایید به نحوه استفاده از رابط های کاربردی در Stream.

روش با محمول

برای مثال، بیایید متد کلاس را در نظر بگیریم Stream- filterکه به عنوان آرگومان در نظر گرفته می‌شود Predicateو Streamفقط عناصری را برمی‌گرداند که شرط را برآورده می‌کنند Predicate. در زمینه Stream-a، این بدان معنی است که فقط از عناصری عبور می کند که وقتی از آنها در یک متد رابط trueاستفاده می کنید، برگردانده می شوند . این همان چیزی است که مثال ما برای آن به نظر می رسد ، اما برای فیلتری از عناصر در : testPredicatePredicateStream
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شامل عناصر "النا"، "جان"، "الکس"، "جیم"، "سارا" خواهد بود. یک روش معمولی نیز وجود دارد 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
یعنی هر کدام از عناصر ما در خودش ضرب می شود و به همین ترتیب برای چهار عدد اول. رابط های کاربردی در جاوا - 4همین! اگر پس از خواندن این مقاله یک قدم به درک و تسلط بر Stream API در جاوا نزدیک شوید، عالی خواهد بود!
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION