CodeGym /Kurslar /JAVA 25 SELF /Funksional proqramlaşdırmada səhvlərin təhlili

Funksional proqramlaşdırmada səhvlərin təhlili

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Lambda ifadələri ilə bağlı səhvlər: dəyişənlərin tutulması

Java-da lambda ifadələri xarici kontekstdən dəyişənlərdən istifadə edə bilər. Lakin bir məhdudiyyət var: belə dəyişənlər final və ya “effektiv final” (effectively final) olmalıdır, yəni ilkinləşdirmədən sonra dəyişdirilməməlidir.

Səhv nümunəsi

int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Kompilyasiya xətası!

Niyə?
Kompilyator etiraz edir: dəyişən lambda daxilində istifadə olunur, deməli o final və ya effectively final olmalıdır, halbuki sum lambda daxilində dəyişdirilir.

Necə qarşısını almaq olar?

  • Xarici dəyişənlər tələb etməyən stream-lərin terminal əməliyyatlarından istifadə edin: mapToInt + sum().
  • Ən son çarə kimi — AtomicInteger kimi konteynerlərdən və ya bir elementli massivdən istifadə edin (amma bu daha çox “hack”dir).
int sum = list.stream().mapToInt(Integer::intValue).sum();

Bənzətmə
Təsəvvür edin ki, lambda “zaman səyahətçisi”dir: o, dəyişənin dəyərini yaradılma anında “yadda saxlayır” və onun sonradan necə dəyişdiyini görə bilmir. Dəyişməyə cəhd — “baba paradoksu”dur, kompilyator proqramı toplamağa imkan verməyəcək.

2. Görünürlük sahəsi və this ilə səhvlər

Lambda ifadəsində this açar sözü anonim sinfə deyil, xarici obyektə aiddir (anonim siniflərdə olduğu kimi deyil).

Nümunə

public class Example {
    int value = 42;

    void foo() {
        Runnable r = () -> {
            System.out.println(this.value); // this — Example-dir, Runnable deyil!
        };
        r.run();
    }
}

Vacibdir: kodu anonim siniflərdən lambda-lara keçirərkən this-in davranışı dəyişir — bunu nəzərə alın ki, gözlənilməz nəticə almayasınız.

3. Dəyişən vəziyyətlə bağlı problemlər (yan təsirlər)

Funksional yanaşma yan təsirlərdən qaçmağı tövsiyə edir: funksiyalar özləri xaricindəki vəziyyəti dəyişmir və xarici kolleksiyaları/dəyişənləri mutasiya etmir.

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();

names.forEach(name -> {
    if (name.startsWith("A")) {
        newNames.add(name); // Yan təsir!
    }
});

Kod “işləyir”, amma daha az proqnozlaşdırılandır və parallelStream() istifadə ediləndə təhlükəlidir (rəqabət şəraiti və istisnalar riski). Test etmək və dəstəkləmək daha çətindir.

Düzgün: xarici vəziyyəti dəyişmədən yeni nəticəni açıq şəkildə formalaşdıran əməliyyatlardan istifadə edin.

List<String> newNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

4. Tiplər və generics ilə səhvlər

Java sərt tipli dildir. Bəzən kompilyator çox mürəkkəb lambda və ya zəncirlərdən tipləri çıxara bilmir.

Nümunə

List<Object> objects = List.of(1, "sətir", 3.14);
List<String> strings = objects.stream()
    .filter(obj -> obj instanceof String)
    .map(obj -> (String) obj)
    .collect(Collectors.toList());

Məntiqli görünür, amma istənilən yazı səhvi və ya yalnış çevirmə kompilyasiya xətasına və ya daha pisi, icra vaxtında ClassCastException-a səbəb ola bilər.

Necə qarşısını almaq olar?

  • Tip çıxarımı “büdrəyirsə”, açıq tiplər əlavə edin.
  • <String> yazmaqdan və ya lambda-ları parametrləşdirməkdən çəkinməyin: (String s) -> ....
  • Çevirmələr zamanı tiplərin uyğunluğunu yoxlayın.

Tipik hal Optional ilə

Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // nəticə — Optional<Integer>

Əgər siz Optional<String> gözləyirdinizsə, amma Optional<Integer> aldınızsa, funksiyanızın nə qaytardığını yoxlayın.

5. Lambda-larda yan təsirlər və paralellizm

Paralel stream-lər (parallelStream()) üstəgəl yan təsirlər — təhlükəli kombinasiyadır.

Nümunə

List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

numbers.parallelStream().forEach(n -> results.add(n)); // TEHLÜKƏLİ!

Nə baş verə bilər?

  • Məlumatların itirilməsi və ya təkrarlanması.
  • ConcurrentModificationException və ya “mistik” bug-lar.

Necə düzgün?

  • Thread-safe kolleksiyalardan istifadə edin: ConcurrentLinkedQueue, CopyOnWriteArrayList.
  • Daha da yaxşısı — ümumiyyətlə yan təsirlərdən qaçın və nəticəni collect(...) ilə toplayın.
List<Integer> results = numbers.parallelStream()
    .map(n -> n)
    .collect(Collectors.toList());

6. Oxunaqlılığın itirilməsi: “stream-spagetti” və uzun zəncirlər

Funksional üslub yaxşıdır, ta ki zəncir “hipermarket qəbzinə” çevrilməyənə qədər.

List<String> result = list.stream()
    .filter(s -> s.length() > 2)
    .map(String::trim)
    .map(s -> s.toUpperCase())
    .filter(s -> s.contains("JAVA"))
    .sorted()
    .distinct()
    .collect(Collectors.toList());

Məsləhətlər:

  • Zəncirləri məntiqi bloklara bölün.
  • Mürəkkəb lambda-ları mənalı adları olan ayrıca metodlara çıxarın.
  • Lazım olduqda — hətta Stream kodunda da — şərhlər əlavə edin.

7. Uğursuz dəyişən və funksiya adları

Həddindən artıq qısa adlar (x, y, z) anlamağı çətinləşdirir.

list.stream()
    .map(x -> x.trim())
    .filter(y -> y.length() > 3)
    .map(z -> z.toUpperCase())
    .forEach(System.out::println);

Məna ifadə edən adlardan istifadə edin, xüsusən də lambda çoxsətirlidirsə və ya qeyri-trivial məntiqi ifadə edirsə.

8. nullOptional ilə səhvlər

Stream API və funksional interfeyslər null-u sevmir. null-un lambda və ya stream-ə ötürülməsi tez-tez NullPointerException-a səbəb olur.

List<String> list = Arrays.asList("a", null, "b");
list.stream()
    .map(String::toUpperCase) // Boom! ikinci elementdə NPE
    .forEach(System.out::println);

Necə düzgün?

  • null-ları əvvəlcədən filtr edin: .filter(Objects::nonNull).
  • Dəyərin olmamasını açıq göstərmək üçün Optional istifadə edin.

9. Birləşdirilmiş funksiyalarda qaytarılan tiplə bağlı problemlər

composeandThen istifadə edərkən funksiyaların tətbiq olunma sırasını və gözlənilən tipləri qarışdırmaq asandır.

Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;

Function<String, Integer> parseAndSquare = parse.andThen(square);
// İşləyir: əvvəl parse, sonra square

Function<String, Integer> squareThenParse = parse.compose(square);
// Xəta! square Integer qəbul edir, parse isə String gözləyir

Nəticə: tətbiq sırasını və tiplərin uyğunluğunu həmişə yoxlayın.

10. Lambda-larda checked istisnalarla bağlı problemlər

java.util.function paketindəki funksional interfeyslər checked istisnaların atılmasına icazə vermir (məsələn, IOException). Əgər lambda daxilində onları atan kod lazımdırsa, istisnanı əl ilə idarə edin.

Function<String, String> readFile = path -> {
    try {
        return Files.readString(Path.of(path));
    } catch (IOException e) {
        throw new RuntimeException(e); // Yaxud başqa cür idarə edin
    }
};

Əks halda, kompilyator belə funksiyanı stream-də və ya kolleksiyada istifadə etməyə icazə verməyəcək.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Funksional proqramlaşdırma
Funksional proqramlaşdırma
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION