1. Funksiyaların kompozisiyası anlayışı
Bir az nəzəriyyə (amma çox da yorucu deyil)
Riyaziyyatda funksiyaların kompozisiyası — bir funksiyanın nəticəsinin digər funksiyanın girişi olmasıdır. Əgər bizdə f və g funksiyaları varsa, g(f(x)) kompozisiyası belədir: əvvəlcə f funksiyasını x-ə tətbiq edirik, sonra nəticəni g-yə veririk.
Proqramlaşdırmada ideya eynidir: mürəkkəb çevrilmələri sadə funksiyalardan yığmaq istəyirik ki, hər şey üçün nəhəng bir funksiyanı yazmayaq. Bu, kodu çevik, yenidən istifadə oluna bilən və oxunaqlı edir.
Şirniyyat konveyerini təsəvvür edin: əvvəl xəmir (f), sonra krem (g), sonra bəzək səpintisi (h). Bütöv proses — h(g(f(inqrediyentlər))).
Niyə kompozisiya vacibdir?
- Kompozisiya proqramı kiçik funksiyalardan ibarət “kublar”dan yığmağa imkan verir.
- Yenidən istifadə rahatdır: bir “kubu” təkrarlamadan müxtəlif yerlərə qoşa bilərsiniz.
- Elastiklik: hər hansı mərhələni dəyişmək lazım olduqda müvafiq funksiyanı əvəz edirik — qalanına toxunmuruq.
- Oxunaqlıq və test edilə bilmə: kiçik funksiyaları oxumaq, yoxlamaq və dəstəkləmək daha asandır.
2. compose və andThen metodları Function interfeysində
Function interfeysi: xatırlatma
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Kompozisiya üçün metodlar:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
- compose: əvvəlcə compose-ə verdiyiniz funksiya icra olunur, sonra — cari funksiya.
- andThen: əvvəlcə cari funksiya icra olunur, sonra — andThen-ə verilən funksiya.
Vizual sxem
// Tutaq ki, iki funksiya var:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;
// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// Amma tiplər fərqlidirsə, sıra önəmlidir!
Nümunə: sətirdən ədədə, sonra kvadrata çevirmə
import java.util.function.Function;
public class ComposeAndThenDemo {
public static void main(String[] args) {
// Funksiya: sətiri ədədə çevirir
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Funksiya: ədədi kvadrata yüksəldir
Function<Integer, Integer> square = x -> x * x;
// Kombinasiya: əvvəl parse, sonra kvadrata yüksəlt
Function<String, Integer> parseThenSquare = parse.andThen(square);
System.out.println(parseThenSquare.apply("7")); // 49
// Əgər yerləri dəyişsək?
// square.compose(parse) — eyni nəticə (bu funksiyalar üçün)
Function<String, Integer> squareOfParsed = square.compose(parse);
System.out.println(squareOfParsed.apply("8")); // 64
}
}
Sıra nə vaxt əhəmiyyət kəsb edir?
Əgər funksiyaların tipləri uyğun gəlmirsə, sıra kritik olur. Məsələn:
Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();
Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11
// Bəs belə:
// length.andThen(addPrefix) — kompilyasiya xətası!
// length Integer qaytarır, addPrefix isə String qəbul edir.
Cədvəl: compose və andThen arasındakı fərq
|
|
|
|
|---|---|---|---|
|
|
|
|
3. Predikatların və digər interfeyslərin kompozisiyası
Predicate<T>: and, or, negate
Funksional interfeys Predicate<T> — nəticəsi boolean olan funksiyadır. Predikatları birləşdirmək üçün xüsusi metodlar var:
- and: məntiqi VƏ (&&)
- or: məntiqi VEYA (||)
- negate: məntiqi DEYİL (!)
Nümunə: mürəkkəb filtr şərtləri
Tutaq ki, bizdə istifadəçi sinfi var:
public class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
İndi bir neçə predikat yazaq:
import java.util.function.Predicate;
Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
// Kombinasiya: yetkin və adı "A" ilə başlayan
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);
// Ya yetkin, ya da adı "A" ilə başlayan
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);
// Yetkin deyil
Predicate<User> notAdult = isAdult.negate();
İndi bu predikatları filtrasiya üçün, məsələn, Stream API ilə istifadə edə bilərik:
import java.util.List;
import java.util.stream.Collectors;
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 17),
new User("Anna", 15),
new User("Mike", 22)
);
List<User> filtered = users.stream()
.filter(adultAndA)
.collect(Collectors.toList());
// filtered siyahısına yalnız Alice düşəcək (yetkindir və adı "A" ilə başlayır)
Consumer, Function, Supplier kompozisiyası
- Consumer<T>: andThen metodu — iki əməliyyatı ard-arda icra etməyə imkan verir.
- Function<T, R>: kompozisiyanı yuxarıda nəzərdən keçirdik.
- Supplier<T>: birbaşa birləşdirilmir, amma digər funksiyaların içində istifadə oluna bilər.
Nümunə: Consumer<T>.andThen
import java.util.function.Consumer;
Consumer<String> print = s -> System.out.println("Alındı: " + s);
Consumer<String> printUpper = s -> System.out.println("Böyük hərflərlə: " + s.toUpperCase());
Consumer<String> combined = print.andThen(printUpper);
combined.accept("hello");
// Çıxış:
// Alındı: hello
// Böyük hərflərlə: HELLO
4. Təcrübə: çevrilmə və filtrasiya zəncirləri
Tapşırıq 1: Function ilə çevrilmə zənciri qurmaq
Tutaq ki, tətbiqimizdə istifadəçiləri "Ad,Yaş" sətiri kimi saxlayırıq, məsələn "Alice,20". Tələb olunur:
- Sətiri User obyektinə çevirmək
- Yaşı əldə etmək
- İstifadəçinin yetkin olub-olmadığını yoxlamaq
import java.util.function.Function;
import java.util.function.Predicate;
Function<String, User> stringToUser = str -> {
String[] parts = str.split(",");
return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;
// Kombinasiya: sətir -> User -> yaş -> predikat
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);
String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Yaş: " + age); // 19
System.out.println("Yetkindirmi? " + isAdultAge.test(age)); // true
Tapşırıq 2: Filtrasiya üçün bir neçə Predicate-i birləşdirmək
Tutaq ki, bizə 18-dən böyük və adı "A" və ya "M" ilə başlayan istifadəçiləri seçmək lazımdır.
Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");
Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));
List<User> filtered = users.stream()
.filter(filter)
.collect(Collectors.toList());
Tapşırıq 3: Çoxmərhələli Function çevrilməsi
Tutaq ki, lazımdır: sətiri götürmək, boşluqları kəsmək, böyük hərflərə çevirmək, "USER: " prefiksi əlavə etmək.
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;
// Zənciri qururuq
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);
System.out.println(pipeline.apply(" vasya ")); // USER: VASYA
5. Funksiyaların kompozisiyasında tipik səhvlər
Xəta №1: compose/andThen ardıcıllığının qarışdırılması.
Yeni başlayanlar tez-tez əvvəl və sonra nə icra olunduğunu qarışdırırlar. Yadda saxlayın: f.compose(g) — əvvəl g, sonra f; f.andThen(g) — əvvəl f, sonra g.
Xəta №2: Tiplərin uyğun gəlməməsi.
Bir funksiyanın nəticə tipi digərinin parametr tipinə uyğun gəlmirsə — kompilyator birləşdirməyə icazə verməyəcək. Məsələn, Function<Integer, String>.andThen(Function<Double, Boolean>) etmək olmaz.
Xəta №3: Həddən artıq mürəkkəb zəncirlər.
Bəzən bütün biznes məntiqini bir zəncirə sığışdırmaq və nəticədə “spagetti” əldə etmək istənilir. Zənciri kiçik funksiyalara bölün və onlara anlaşılan adlar verin.
Xəta №4: Funksiyalarda yan təsirlər.
Funksiya və predikatları “təmiz” saxlamaq daha yaxşıdır (yan təsirlər olmadan), əks halda kompozisiya təhlükəli və qeyri-müəyyən olur.
GO TO FULL VERSION