среда, 1 августа 2012 г.

module: java generics

    Рассмотрим следующую задачу: нам нужно иметь класс/классы, который(-ые) свои полем содержит(-ат) то String, то int[].

    Мы можем завести отдельные классы на каждый тип:
public class StringHolder {
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}
public class IntArrayHolder {
    private int[] data;

    public int[] getData() {
        return data;
    }

    public void setData(int[] data) {
        this.data = data;
    }
}
    Все отлично, за исключением того, что каждого нового хранимого типа придется создавать отдельный класс "хранитель" (holder).

    Можно воспользоваться тем, что у всех ссылочных типов есть общий предок - java.lang.Object и сделать "универсальное хранилище"
public class ObjectHolder {
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

    Оно неплохо работает, хотя и приходится делать явное приведение типов
public class HolderStringTest {
    public static void main(String[] args) {
        ObjectHolder holder = new ObjectHolder();
        holder.setData("Hello!");
        
        String str = (String) holder.getData();
        System.out.println(str);
    }
}
>> Hello!

    Или с int[]   
import java.util.Arrays;

public class HolderIntArrayTest {
    public static void main(String[] args) {
        ObjectHolder holder = new ObjectHolder();
        holder.setData(new int[]{10, 20, 30});

        int[] arr = (int[]) holder.getData();
        System.out.println(Arrays.toString(arr));
    }
}
>> [10, 20, 30]

    Надо понимать, что явные приведения типов это обработка ситуации, когда компилятор не может удостовериться в правильности программы с точки зрения типов. 
    В нашем примере проблемы возникают, когда мы забыли, что положили в хранилище: 
x
public class HolderWrongTest {
    public static void main(String[] args) {
        ObjectHolder holder = new ObjectHolder();
        holder.setData(new int[]{10, 20, 30});

        String str = (String) holder.getData();
        System.out.println(str);
    }
}
>> Exception in thread "main" java.lang.ClassCastException: [I cannot be cast to java.lang.String

    Решением являются generics    
public class GenericHolder<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

    Отметьте отсутствие явного приведения типов в примере использования GenericHolder ниже
import java.util.Arrays;

public class GenericHolderTest<T> {
    public static void main(String[] args) {
        // set String
        GenericHolder<String> strHolder = new GenericHolder<>();
        strHolder.setData("Hello!");
        // get String
        String str = strHolder.getData();
        System.out.println(str);

        // set int[]
        GenericHolder<int[]> intArrayHolder = new GenericHolder<>();
        intArrayHolder.setData(new int[] {10, 20, 30});
        // get int[]
        int[] arr = intArrayHolder.getData();
        System.out.println(Arrays.toString(arr));        
    }
}
    Генерики - это способ полностью определить поведение (класс GenericHolder) не определив для кого он применим. "Доопределение" происходит в момент создания экземпляра.

    Не компилируется!
import java.util.Arrays;

public class WrongGenericHolderTest<T> {
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setData("Hello!");

        int[] arr = holder.getData();
        System.out.println(Arrays.toString(arr));
    }
}

    Явное приведение типов тоже не помогает, компилятор зорко следит за соответствием типов:
    Не компилируется!
import java.util.Arrays;

public class WrongGenericHolderTest<T> {
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setData("Hello!");

        int[] arr = (int[]) holder.getData();
        System.out.println(Arrays.toString(arr));
    }
}
x

Обмануть можно, но придется очень сильно постараться:

import java.util.Arrays;

public class WrongGenericHolderTest<T> {
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setData("Hello!");

        int[] arr = (int[]) (Object) holder.getData();
        System.out.println(Arrays.toString(arr));
    }
}

x
x
x
x
x
x
x
x
x



    Генерики в java - это пример параметрического полиморфизма[1, 2]. Он реализован во многих популярных объектно-ориентированных языках программирования. Но он реализуется различными способами:
- в Java через type erasure
- в C# через reinfication
- в C++ через templates
    Что это значит? Почему приняты такие решения в каждом из языков? Какие преимущества и недостатки этих решений?

P.S. В каком случае можно использовать:
- new T();
- new T[];?