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. null və Optional 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
compose və andThen 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.
GO TO FULL VERSION