Java Generics: Type ආරක්ෂාවට හොඳම මග | Java SC Guide

Java Generics: Type ආරක්ෂාවට හොඳම මග | Java SC Guide

ඉතින් කොහොමද යාලුවනේ! අද අපි කතා කරන්න යන්නේ Java වල ඉතාම වැදගත්, ඒ වගේම පොඩ්ඩක් සංකීර්ණ වෙන්න පුළුවන් වුණත්, හරියට තේරුම් ගත්තොත් අපේ කෝඩ් එක මාර විදියට ශක්තිමත් කරන මාතෘකාවක් ගැන – ඒ තමයි Java Generics. ඔයාලා Java Collection Framework එක (ArrayList, HashMap වගේ) පාවිච්චි කරද්දි <String>, <Integer> වගේ දේවල් දැකලා ඇතිනේ. ඒ තමයි Generics. ඒක මොකටද පාවිච්චි කරන්නේ කියලා පැහැදිලිව දන්නේ නැති කෙනෙක්ට, මේ ලිපිය ඒකට හොඳම ආරම්භයක් වෙයි.

අපි මේ ගැන කතා කරද්දි, Type Safety, Code Reusability, ඒ වගේම අපි කොහොමද Common Runtime Errors අඩු කරගන්නේ කියලා බලමු. මේක අපේ දිනපතාම කරන coding වැඩවලට ගොඩක් ප්‍රයෝජනවත් වෙනවා අනිවාර්යයෙන්ම.

Java Generics කියන්නේ මොනවද?

සරලවම කිව්වොත්, Generics කියන්නේ Java වල තියෙන feature එකක්, අපිට Classes, Interfaces, Methods හදන්න පුළුවන් ඕනෑම data type එකකට (like Integer, String, CustomObject) වැඩ කරන්න පුළුවන් වෙන විදියට. ඒක නිකන් template එකක් වගේ. අපි මේක පාවිච්චි කරන්නේ Compile-time type safety ලබාගන්න. ඒ කියන්නේ, අපේ කෝඩ් එකේ වැරදි තියෙනවා නම් ඒක වැඩ කරන වෙලාවේ (runtime) නොවී, කෝඩ් එක ලියන වෙලාවෙම (compile-time) පෙන්නලා දෙන එක.

Generics නැතුව අපි Collection එකක් පාවිච්චි කරද්දි තිබ්බ ගැටලුවක් තමයි Object type එක පාවිච්චි කරන්න වෙන එක. එතකොට අපිට Collection එකට ඕනෑම type එකක object එකක් add කරන්න පුළුවන්. ඒක ගන්න වෙලාවට අපිට හරියටම දන්නේ නැහැ මොන type එකේ object එකක්ද තියෙන්නේ කියලා. ඒ නිසා අපිට explicit type casting කරන්න වෙනවා. සමහර වෙලාවට එතනදි වැරදීමක් වුණොත්, ClassCastException කියන runtime error එක එන්න පුළුවන්. ඒක නම් ලොකු headache එකක් නේද? Generics මේ headache එක විසඳනවා.

උදාහරණයක් විදියට මේ කෝඩ් කෑල්ල බලන්න:

Generics නැතුව:

List list = new ArrayList();
list.add("Hello"); // String එකක් add කළා
list.add(123);   // Integer එකක් add කළා - වැරදීමක් නෑ මෙතනදි

// මෙතන තමයි අවුල එන්නේ. මේක String එකක් කියලා හිතලා cast කරනවා
String s = (String) list.get(0); // OK
// String s2 = (String) list.get(1); // Runtime error! ClassCastException
System.out.println(s);

Generics එක්ක:

List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // Compile-time error! මෙතනම වරද පෙන්නනවා

String s = stringList.get(0); // Cast කරන්න ඕනෙත් නෑ, Type Safety එක සහතිකයි.
System.out.println(s);

දැන් තේරෙනවනේ Generics වලින් අපිට ලැබෙන වාසිය? Runtime errors වලින් අපිව බේරගන්නවා. ඒක නියමයි නේද?

Generic Classes හදමු!

අපි බලමු Generics පාවිච්චි කරලා කොහොමද Class එකක් හදන්නේ කියලා. හිතන්න අපිට ඕන කරනවා ඕනෑම type එකක වටිනාකමක් ගබඩා කරන්න පුළුවන් Box එකක් වගේ Class එකක් හදන්න. සාමාන්‍යයෙන් අපි Object type එක පාවිච්චි කරන්න පුළුවන් වුණත්, ඒක safe නෑ කියලා අපි දැන් දන්නවා.

public class Box<T> {
    private T t; // T stands for "Type" - Generic Type Parameter එක

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

මේ <T> කියන එකෙන් කියන්නේ මේ Box Class එක Generic Class එකක් කියලා. T කියන්නේ Type Parameter එක. අපිට T වෙනුවට ඕනෑම අකුරක් (E for Element, K for Key, V for Value වගේ) පාවිච්චි කරන්න පුළුවන්. හැබැයි T කියන එක තමයි convention එක.

දැන් මේ Box Class එක කොහොමද පාවිච්චි කරන්නේ කියලා බලමු:

public class GenericClassDemo {
    public static void main(String[] args) {
        // Integer type එකක් තියෙන Box එකක් හදමු
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(10);
        // integerBox.set("Hello"); // Compile-time error! මේක String එකක්, Integer නෙවෙයි
        Integer someInt = integerBox.get(); // Cast කරන්න ඕනෙ නෑ
        System.out.println("Integer Box value: " + someInt); // Output: Integer Box value: 10

        // String type එකක් තියෙන Box එකක් හදමු
        Box<String> stringBox = new Box<>(); // Java 7න් පස්සේ <> (diamond operator) පාවිච්චි කරන්න පුළුවන්
        stringBox.set("Java Generics is awesome!");
        String someString = stringBox.get();
        System.out.println("String Box value: " + someString); // Output: String Box value: Java Generics is awesome!

        // Custom object එකක් වුණත් වැඩ කරනවා!
        Box<LocalDateTime> dateTimeBox = new Box<>();
        dateTimeBox.set(LocalDateTime.now());
        LocalDateTime now = dateTimeBox.get();
        System.out.println("Date Time Box value: " + now);
    }
}

දැක්කනේ? කොච්චර මාර ලේසියෙන් අපිට Box Class එක ඕනෑම data type එකකට පාවිච්චි කරන්න පුළුවන්ද කියලා. Compiler එක වැඩ කරන වෙලාවෙම අපේ වැරදි අල්ලගන්නවා. Runtime එකට යන්න දෙන්නේ නෑ.

Generic Methods හදමු!

Generics Classes වලට විතරක් නෙවෙයි, methods වලටත් පාවිච්චි කරන්න පුළුවන්. ඒකෙන් අපිට පුළුවන් එකම method එකක් විවිධ data types වලට වැඩ කරන්න හදන්න.

public class Util {
    // මේක Generic Method එකක්. <T> return type එකට කලින් තියෙන්නේ.
    public static <T> void printArray(T[] array) {
        System.out.print("Array Elements: ");
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    // Generic method එකක්, return type එකත් Generic වෙන්න පුළුවන්
    public static <T> T getFirstElement(List<T> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }
}

මෙතනදි <T> කියන Type Parameter එක method එකේ return type එකට කලින් දාන්න ඕනේ. ඒකෙන් කියන්නේ මේ method එක Generic Method එකක් කියලා.

දැන් මේ Generic Methods කොහොමද පාවිච්චි කරන්නේ කියලා බලමු:

public class GenericMethodDemo {
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"Apple", "Banana", "Orange"};
        Double[] doubleArray = {1.1, 2.2, 3.3};

        System.out.println("Printing Integer Array:");
        Util.printArray(intArray); // Output: Array Elements: 1 2 3 4 5 

        System.out.println("Printing String Array:");
        Util.printArray(stringArray); // Output: Array Elements: Apple Banana Orange 

        System.out.println("Printing Double Array:");
        Util.printArray(doubleArray); // Output: Array Elements: 1.1 2.2 3.3 

        List<String> fruits = Arrays.asList("Mango", "Grape", "Pineapple");
        String firstFruit = Util.getFirstElement(fruits);
        System.out.println("First fruit: " + firstFruit);

        List<Integer> numbers = Arrays.asList(100, 200, 300);
        Integer firstNumber = Util.getFirstElement(numbers);
        System.out.println("First number: " + firstNumber);
    }
}

එකම printArray method එකෙන් විවිධ array types handle කරන්න පුළුවන් වීම නියමයි නේද? Code duplicate කරනවා වෙනුවට මේ වගේ Generic Methods පාවිච්චි කරන එක හරිම පහසුයි, smart වැඩක්!

Generic Interfaces ගැනත් පොඩ්ඩක් බලමු!

Classes සහ Methods වගේම Interfaces වලටත් Generics පාවිච්චි කරන්න පුළුවන්. මේකෙන් අපිට පුළුවන් Interface එකක් define කරන්න, ඒක Implement කරන Class එකට, මොන data types එක්කද වැඩ කරන්නේ කියලා තීරණය කරන්න පුළුවන් වෙන විදියට.

උදාහරණයක් විදියට අපි Converter Interface එකක් හදමු. මේකෙන් පුළුවන් එක data type එකක් තව data type එකකට convert කරන්න.

public interface MyConverter<S, D> {
    // S: Source Type, D: Destination Type
    D convert(S source);
}

දැන් මේ MyConverter Interface එක Implement කරන Classes දෙකක් බලමු:

// String එකක් Integer එකකට convert කරන Class එකක්
public class StringToIntegerConverter implements MyConverter<String, Integer> {
    @Override
    public Integer convert(String source) {
        try {
            return Integer.parseInt(source);
        } catch (NumberFormatException e) {
            System.err.println("Invalid number format: " + source);
            return null;
        }
    }
}

// Integer එකක් String එකකට convert කරන Class එකක්
public class IntegerToStringConverter implements MyConverter<Integer, String> {
    @Override
    public String convert(Integer source) {
        if (source == null) {
            return null;
        }
        return String.valueOf(source);
    }
}

මේවා කොහොමද පාවිච්චි කරන්නේ කියලා බලමු:

public class GenericInterfaceDemo {
    public static void main(String[] args) {
        MyConverter<String, Integer> sToIntConverter = new StringToIntegerConverter();
        Integer number = sToIntConverter.convert("123");
        System.out.println("Converted String to Integer: " + number); // Output: Converted String to Integer: 123

        MyConverter<Integer, String> intToStringConverter = new IntegerToStringConverter();
        String text = intToStringConverter.convert(456);
        System.out.println("Converted Integer to String: " + text); // Output: Converted Integer to String: 456

        Integer invalidNumber = sToIntConverter.convert("abc");
        System.out.println("Converted invalid string: " + invalidNumber); // Output: Converted invalid string: null (and error message)
    }
}

මේ වගේ Generic Interfaces වලින් අපිට පුළුවන් විවිධ data types අතර conversion logic එක පැහැදිලිව maintain කරන්න. ඒක code maintainability එකටත්, readability එකටත් ගොඩක් හොඳයි.

Wildcards (වයිල්ඩ්කාඩ්) - ටිකක් ගැඹුරට!

Generics ගැන කතා කරද්දි, Wildcards ගැන කතා නොකරම බෑ. මේවා ? (question mark) එකෙන් පෙන්නනවා. මේවා විශේෂයෙන්ම collections එක්ක වැඩ කරද්දි ගොඩක් වැදගත් වෙනවා. අපි List<String> එකක් List<Object> එකකට assign කරන්න බෑ, මොකද ඒවා එකිනෙකට වෙනස් type කියලා Java සලකන නිසා. හැබැයි Wildcards වලින් අපිට පුළුවන් මේ type restrictions ටිකක් ලිහිල් කරන්න.

ප්‍රධාන Wildcards වර්ග දෙකක් තියෙනවා:

? super T (Lower Bounded Wildcard)

මේකෙන් කියන්නේ T කියන ටයිප් එකේ නැත්නම් T එකේ Super class එකක object එකක් කියන එක. උදාහරණයක් විදියට, List<? super Integer> කියන්නේ Integer class එකේ නැත්නම් Integer class එකේ Super class (Number, Object වගේ) එකක object list එකක්.මේක write-only operations වලට ගොඩක් සුදුසුයි. ඒ කියන්නේ, මේ list එකට data add කරන්න පුළුවන් (T type එකේ object එකක් නැත්නම් T එකේ child type එකක object එකක් add කරන්න පුළුවන්). හැබැයි ඒකෙන් data retrieve කරද්දි අපිට ගොඩක් වෙලාවට Object type එක විදියට තමයි ගන්න වෙන්නේ.

public class WildcardDemo {
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        addIntegers(numberList);
        System.out.println("Number List after adding integers: " + numberList); // Output: Number List after adding integers: [10, 20, 30]

        List<Object> objectList = new ArrayList<>();
        addIntegers(objectList);
        System.out.println("Object List after adding integers: " + objectList); // Output: Object List after adding integers: [10, 20, 30]

        List<? super Integer> myList = new ArrayList<Number>();
        myList.add(new Integer(50)); // Can add Integer
        myList.add(new Integer(60));
        // Integer val = myList.get(0); // Compile-time error! Can't guarantee it's an Integer
        Object obj = myList.get(0); // Only safe to retrieve as Object
        System.out.println("Value from super list: " + obj);
    }

    public static void addIntegers(List<? super Integer> list) {
        // මේ method එකට Integer නැත්නම් Integer එකේ Super ඕනෑම List එකක් දෙන්න පුළුවන්.
        list.add(10);
        list.add(20);
        list.add(30);
    }
}

? extends T (Upper Bounded Wildcard)

මේකෙන් කියන්නේ T කියන ටයිප් එකේ නැත්නම් T එකේ Child class එකක object එකක් කියන එක. උදාහරණයක් විදියට, List<? extends Number> කියන්නේ Number class එකේ නැත්නම් Number class එකේ Child class (Integer, Double, Float වගේ) එකක object list එකක්.මේක read-only operations වලට ගොඩක් සුදුසුයි. ඒ කියන්නේ, මේ list එකෙන් data ගන්න පුළුවන්, හැබැයි ඒකට අලුතින් data add කරන්න බැහැ (null හැර). මොකද compiler එකට හරියටම දන්නේ නෑ මොන actual type එකක්ද තියෙන්නේ කියලා.

public class WildcardDemo {
    public static void main(String[] args) {
        List<Integer> integerList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

        processNumbers(integerList);
        processNumbers(doubleList);

        List<? extends Number> numList = new ArrayList<Integer>();
        // numList.add(new Double(1.0)); // Compile-time error! Can't add
        // numList.add(new Integer(1)); // Compile-time error! Can't add
        numList.add(null); // null නම් add කරන්න පුළුවන්
        Number num = numList.get(0); // Number විදියට retrieve කරන්න පුළුවන්
        System.out.println("First number (from Integer list via extends): " + num);
    }

    public static void processNumbers(List<? extends Number> list) {
        // මේ method එකට Number නැත්නම් Number එකේ Child ඕනෑම List එකක් දෙන්න පුළුවන්.
        for (Number n : list) {
            System.out.print(n + " ");
        }
        System.out.println();
    }
}

මේ Wildcards ටිකක් සංකීර්ණ වුණත්, ලොකු project වලදි Collection Framework එක්ක වැඩ කරද්දි මේවා ගොඩක් වැදගත් වෙනවා. PECS (Producer-extends, Consumer-super) කියලා rule එකක් තියෙනවා, ඒක මතක තියාගන්න පුළුවන් මේවා කවදද පාවිච්චි කරන්නේ කියලා තේරුම් ගන්න. Producer කියන්නේ data ලබා දෙන Collection (ඔය list එකෙන් get කරනවා නම්), Consumer කියන්නේ data එකක් දාන Collection (ඔය list එකට add කරනවා නම්).

කවදද Generics පාවිච්චි කරන්නේ? (Best Practices)

  • Type safety අවශ්‍ය හැම වෙලාවකම: විශේෂයෙන්ම Collection Framework එක්ක වැඩ කරද්දි, ClassCastException වගේ Runtime errors වලින් බේරෙන්න.
  • Code Reusability: එකම කෝඩ් block එකක් විවිධ data types වලට වැඩ කරන්න හදන්න. Generic methods, classes, interfaces වලින් මේක හොඳටම කරන්න පුළුවන්.
  • Clarity and Readability: ඔබේ කෝඩ් එකේ මොන data type එකක්ද use කරන්නේ කියලා පැහැදිලිව පෙන්නනවා. නොදන්නා කෙනෙක්ට ඔබේ කෝඩ් එක කියවද්දි පහසු වෙනවා.
  • Java Collection Framework: ArrayList<T>, HashMap<K, V> වගේ collections වලට Generics අනිවාර්යයෙන්ම පාවිච්චි කරන්න.
  • Naming Conventions: Generic Type Parameter එකට Capital letter එකක් පාවිච්චි කරන්න (T for Type, E for Element, K for Key, V for Value).

අවසන් වශයෙන්

Java Generics කියන්නේ Java Developer කෙනෙක් විදියට අනිවාර්යයෙන්ම දැනගෙන ඉන්න ඕන මාතෘකාවක්. මුලදි පොඩ්ඩක් තේරුම් ගන්න අමාරු වුණත්, මේක තේරුම් ගත්තාට පස්සේ ඔබේ කෝඩ් එකේ ගුණාත්මක භාවය, ශක්තිමත් භාවය සහ පිරිසිදු භාවය (cleanliness) ගොඩක් වැඩිදියුණු වෙනවා. මේක ප්‍රායෝගිකව කරලා බලන්න. පොඩි project එකක් හදලා මේවා implement කරලා බලන්න.

මේ ලිපිය ගැන ඔබේ අදහස්, ප්‍රශ්න පහතින් comment කරන්න. Generics ගැන තව දැනගන්න ඕනද? එහෙනම් ඒකත් කියන්න. තව අලුත් ලිපියකින් හමුවෙමු! Happy Coding!