C, PHP, VB, .NET

Дневникът на Филип Петров


* Генерични методи и класове

Публикувано на 06 септември 2015 в раздел ПИК3 Java.

При динамичния полиморфизъм показахме как може един метод да приеме за входен параметър родителски клас или интерфейс и по този начин неговият код да може да обработва всички негови наследници. Например ако ни се налага да напишем метод за сортиране на списък, ние може да му подаден като входен параметър елемент от тип интерфейса List и да работим с него. Впоследствие на този метод можете да подадем ArrayList, LinkedList, Vector и т.н. и метода ще сортира всеки един от тях без да се интересува от техните специфики. Така вместо да напишем различен метод за сортиране за всеки вид списък, пишем само един, който работи успешно с всички видове.

Генеричните типове са продължение на тази идея, което я извежда още по-далеч – вече може да правите методи, които не просто работят с всички еднотипни обекти, а работят с каквито и да е обекти, включително ако между тях няма наследствени връзки. Особено удобни са за „контейнери“ на информация – когато не се интересуваме от действията, които извършва подадения обект, а просто искаме да съхраним неговата информация. Вече сте виждали това при колекциите (Collections) – например ArrayList<String> и ArrayList<Integer> си е все един и същи клас ArrayList, който е генеричен и работи с каквито и да е обекти (типа им указваме чрез ъгловите скоби). Тук първо ще дадем кратки примери, а в края на статията ще покажем един обобщен.

Генерични методи

Нека имаме клас, който работи с различни видове обекти. Искаме да напишем статичен метод, който отпечатва съдържанието на подаден обект чрез System.out.println, без да се интересуваме от типа на самия обект. Ето как ще направим това:

public class GenTest{
  public static void main(String[] args){
    GenUtil.printer("Hello");
    GenUtil.printer(1);
    GenUtil.printer(new GenTest());
  }
}

class GenUtil{
  static <T> void printer(T var){
    System.out.println(var);
  }
}

Както виждате с ъглови скоби преди връщания резултат от метода (в нашия пример void) е указано, че е генеричен метод, който ще работи с един тип данни – обобщаваме го като тип „T“. Методът от примера ни приема променлива от този тип и я отпечатва на екрана. В клас GenTest показваме, че можем да подаваме каквито и да е обекти – дори безсмислени за отпечатване на екрана (не са направили overload на своя метод toString), както в примера ни „new GenTest()“.

Динамичен полиморфизъм чрез генерични методи

Можем да реализираме динамичен полиморфизъм чрез генерични методи като фиксираме генеричния тип да бъде наследник на даден клас или интерфейс. Това става като в ъгловите скоби добавим „extends“ след името на генеричния тип. По този начин ще укажем на компилатора, че обектите с които работим трябва да са наследници на указания клас или интерфейс.

public class Test{
  public static void main(String[] args){
    System.out.println(GenTest.min(1,-3,0));
    System.out.println(GenTest.min("AbC","CCC","AOO"));
    System.out.println(GenTest.min('V','K','X'));
  }
}

class GenTest{
  static <T extends Comparable<T>> T min(T a, T b, T c){
    return (a.compareTo(b)<0)?((a.compareTo(c)<0)?a:c):((b.compareTo(c)<0)?b:c);
  }
}

* Забележете, че самия интерфейс Comparable е генеричен сам по тебе си!

Някои генерични методи могат да бъдат пренаписани по алтернативен начин чрез използването на динамичен полиморфизъм. Например задачата от по-предишния пример може да бъде пренаписана по следния начин:

  static void printer(Object var){
    System.out.println(var);
  }

Тази техника обаче е с по-ограничено действие и не може да реши всички видове задачи. Когато методът трябва да върне резултат от генеричен тип, с обикновения динамичен полиморфизъм ще трябва да връщаме „Object“ (или някакъв общ интерфейс или обект), а после трябва да проверяваме типа му чрез „instanceof“ преди да го използваме. Затова можем да кажем, че генеричните типове са разширение, надграждане на динамичния полиморфизъм.

Генерични класове

Често в един клас ще искаме да напишем повече от един генеричен метод. Освен това често ще искаме различните генерични методи да работят с общи генерични член променливи. Така ни се налага по някакъв начин да синхронизираме генеричните методи така, че да работят с един и същи тип. Това се получава чрез указване на генеричния тип като общ за целия клас. В долния пример добавяме <T> след името на класа. Това означава, че при създаването на класа се указва генеричния тип и от там нататък класа ще работи само с този тип:

public class Test{
  public static void main(String[] args){
    GenUtil<String> gen = new GenUtil<>();
    gen.set("Hello");
    System.out.println(gen.get());
    
    GenUtil<Integer> gen2 = new GenUtil<>();
    gen2.set(new Integer(2));
    System.out.println(gen2.get());
  }
}

class GenUtil<T>{
  private T element;
  T get(){
    return this.element;
  }
  void set(T t){
    this.element = t;
  }
}

Вие вече нееднократно сте виждали подобни примери с ArrayList, LinkedList и др.

Задача: Да се моделира „K списък“ със следните качества – той притежава същите характеристики както стандартния едносвързан списък, но за всеки елемент освен връзка към следващ елемент се пази и връзка с K-ти елемент напред. По този начин може да се осъществи евентуално по-бързо търсене. Например при 10 ако търсите 62ри елемент стандартно ще направите 62 операции със стандартната функция get(62):

1 -> 2 -> 3 -> … -> 61 -> 62

Докато използвайки модифицирания метод get(62, true) търсенето ще бъде с 9 операции:

1 -> 10 -> 20 -> 30 -> 40 -> 50 -> 60 -> 61 -> 62

Направете тест за бързодействие.

Решение: В предложеното решение демонстрираме употребата на три похвата – генерични типове, вложени класове и анонимни класове. В main метода първоначално запълваме списък с произволни елементи, след което пускаме две нишки – едната със стандартно търсене елемент по елемент, а другата с “k търсене”.

public class KListExampleProgram{
  public static void main(String[] args){
    final KList<Integer> list1 = 
      new KList<Integer>(1999);
    final KList<Integer> list2 = 
      new KList<Integer>(1999);
    for(int i=0; i<10000; i++){
      Integer el = new Integer((int)(Math.random()*10000));
      list1.add(el);
      list2.add(el);
    }
    Thread t1 = new Thread(){
      public void run(){
        for(int i=0; i<500000; i++){
          list1.get(9000);
        }
        System.out.println("RegularList finished");
      }
    };
    Thread t2 = new Thread(){
      public void run(){
        for(int i=0; i<500000; i++){
          list2.get(9000, true);
        }
        System.out.println("KList finished");
      }
    };
    t1.start();
    t2.start();
    try{
       t1.join();
       t2.join();
    }
    catch(InterruptedException e){}
    System.out.println("Main finished");
  }
} // край на KListExampleProgram
class KList<T>{
  private Node<T> header;
  int k;
  int length;
  public KList(int k) {
    header = new Node<T>(null);
    length = 0;
    this.k = k;
  }
  // добавя елемент в края на списъка
  void add(T element){
    Node<T> newNode = new Node<T>(element);
    Node<T> temp = this.header;
    int kBack = this.length+1-this.k;
    int counter = 0;
    while(temp.next != null){
      temp = temp.next;
      counter++;
      if(counter == kBack) temp.kthNode = newNode;
    }
    temp.next = newNode;
    this.length++;
  }
  
  T get(int n) throws IndexOutOfBoundsException{
    return this.get(n, false);
  }
  T get(int n, boolean useK) throws IndexOutOfBoundsException{
    if(n>=this.length) throw new IndexOutOfBoundsException("No such element");
    Node<T> temp = this.header.next;
    if(useK == true){
      int bigStep = n/this.k;
      for(int i=0; i<bigStep; i++){
        temp = temp.kthNode;
      }
      int smallStep = n%this.k;
      for(int i=0; i<smallStep; i++){
        temp = temp.next;
      }
      return temp.element;
    }
    else{
      for(int i=0; i<n; i++){
        temp = temp.next;
      }
      return temp.element;
    }
  }  
  private class Node<T>{
    T element;
    Node<T> next;
    Node<T> kthNode;
    Node(T element) {
      this(element, null);
    }
    Node(T element, Node<T> next) {
      this.element = element;
      this.next = next;
      Node<T> kthNode = null;
    }
  } // край на вложен клас Node
} // край на клас KList

Допълнение (от инж. Десислав Андреев):

Друг интересен пример за използване на генерични типове е DAO (data access object). Бидейки изключително разпространен начин за запазване и подсигуряване на данни, неговият потенциал не се използва докрай. Повечето предложени имплементации дори не стъпват на принципа DRY. Също така трябва да отбележим, че неговото използване е най-често срещано именно в Java EE приложенията, макар че може да се прилага при повечето от програмните езици. Както знаем, генеричните методи ни помагат да създадем преизползваем код, който ще се верифицира на ниво компилация – така ще сме сигурни, че работим с правилните типове и ще спечелим производителност по време на изпълнение. Освен това, подобни имплементации са директни примери за статичен полиморфизъм и също спомагат за спазването на ООП концепциите. Преди да продължим нека изкажем фундаменталната теорема на софтуерното инженерство: „We can solve any problem by introducing an extra level of indirection“ (Butler Lampson).

Нека имаме интерфейси, които покриват CRUD (Create, read, update and delete) и DAO, с които да ги имплементираме. По-досетливите ще видят веднага, че за всеки базов клас, който бива разширен с DAO, ще трябва да пише различен код. След като имплементираме CRUD функционалността, то единствените разлики ще бъдат в типовете на параметрите на функциите:

class ProducerDAO{
	public void create(Producer p){...}
}

class ClientDAO{
	public void create(Client c){...}
}

Оттук нататък нашата задача е да направим такъв интерфейс (и базов клас), който е типово защитен и да позволява една и съща имплементация:

public interface IDAOBase<T> {
	public void create(T domain);
	public void read(T domain);
	public void update(T domain);
	public void delete(T domain);
}

Следва да създадем абстрактен клас, който да представи въпросната функционалност:

public abstract class DAOBase<T> implements IDAOBase<T> {
    @PersistenceContext
    protected EntityManager entityManager; 
    // http://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html
    //типът на обекта ще се пази в променлива от тип Class
    private Class<T> type; 

    public DAOBase() {
        //тъй като не искаме да конкретизираме за 
        //определен тип, използваме java.lang.reflect.Type;
        Type t = getClass().getGenericSuperclass();
        //необходимо е, за да разберем типа на обекта: 
        //java.lang.reflect.ParameterizedType; 
        ParameterizedType pt = (ParameterizedType) t; 

        type = (Class) pt.getActualTypeArguments()[0];
    }

    @Override
    public T create(final T t) {
        this.entityManager.persist(t);
        return t;
    }

    @Override
    public T read(final Object id) {
        //фунцкията, която се имплементира в EntityManager е
        //find. Нашият wrapper се казва read, за да спазва 
        //интерфейса.
        return (T) this.entityManager.find(type, id); 
    }

    @Override
    public T update(final T t) {
        return this.entityManager.merge(t); //подобно на read
    }

    @Override
    public void delete(final Object id) {
        //подобно на read и update
        this.entityManager.remove(
             this.entityManager.getReference(type, id)
        );
    }
}

По-любознателните ще забележат, че прилагаме т. нар. reflection. По-нататък ще дадем примери и за тази способност на Java, но засега да обясним накратко – нека имаме обект от неясен тип и искаме да извикаме негов метод Work(). Бързо можете да се досетите, че това няма да стане, освен ако обектът не съответства на даден интерфейс. Точно тук reflection идва на помощ – чрез този похват нашият код може да провери дали обектът съдържа Work() и след това ще можем да го извикаме. Типичен пример са анотациите и JUnit използва именно reflection, за да следи методите в класовете и да ги извиква по време на тест.

А сега идва ред да напишем и конкретните класве, които биха се възползвали от горните редове. Нека да се върнем в началото и предположим, че имаме вече клас Client:

public interface ClientDAO extends IDAOBase<Client>{
    //връща Client; за упражнение не работим със String username
    public Client returnClientByClientid(Int id); 
}

Самата имплементация на метода ще бъде реализирана в класa:

//Приемаме, че използваме Spring 2.5 и нагоре, както и java.util.List
@Component("clientDao")
public class ClientBase extends DAOBase<Client>
                        implements ClientDAO {
    public Client returnClientByClientid(Int id) {
        Query query = this.entityManager
           .createQuery("select c FROM Client c where c.id= :id");
        query.setParameter("id", id);
        List clients = query.getResultList();
        if (clients != null && clients.size() == 1) {
            return clients.get(0);
        }
        return null;
    }
}

Нека да направим интерфейс, който ще ни служи за създаване на нов клиент

public interface ClientService { 

    @Transactional
    //org.springframework.transaction.annotation.Transactional;
    void createClient(String cname, String cpass); 
}

…и класът, който ще имплементира метода:

//Използваме org.springframework.beans.factory.annotation.Autowired; 
//и org.springframework.stereotype.Service;

@Service("clientService")
public class ClientServiceBase implements ClientService {

    @Autowired
    private ClientDAO dao;

    @Override
    public void createClient(String cname, String cpass) {
        Client c = new Client();
        c.setUsername(cname);
        c.setPassword(cpass);
        dao.create(c);
    }
}

Изводът дотук е, че това е много гъвкав, кратък и сигурен начин, за да изградите медиатор между потребителския интерфейс и базата данни. Остава само да си отговорим на следния въпрос – защо използвахме толкова семпло JPA Entity Manager, а не използвахме само него за имплементацията? Нямаше ли кодът да е по-кратък и по-сигурен… все пак ще използваме готова функционалност? Оставяме на вас да съпоставите custom DAO и Entity Manager. Междувременно може да изпробвате другите методи (и другите класове, например Producer) и да се уверите, че кодът може да бъде преизползван (и да е type-safe).

 



Добави коментар

Адресът на електронната поща няма да се публикува


*