JavaRush /Blog Java /Random-PL /Przerwa kawowa #130. Jak poprawnie pracować z tablicami J...

Przerwa kawowa #130. Jak poprawnie pracować z tablicami Java - wskazówki od Oracle

Opublikowano w grupie Random-PL
Źródło: Oracle Praca z tablicami może obejmować odbicie, wyrażenia generyczne i wyrażenia lambda. Niedawno rozmawiałem z kolegą, który programuje w C. Rozmowa zeszła na temat tablic i tego, jak działają w Javie w porównaniu z C. Wydało mi się to trochę dziwne, biorąc pod uwagę, że Java jest uważana za język podobny do C. Tak naprawdę mają wiele podobieństw, ale są też różnice. Zacznijmy prosto. Przerwa kawowa #130.  Jak poprawnie pracować z tablicami Java - wskazówki od Oracle - 1

Deklaracja tablicy

Jeśli postępujesz zgodnie z samouczkiem Java, zobaczysz, że istnieją dwa sposoby zadeklarowania tablicy. Pierwsza jest prosta:
int[] array; // a Java array declaration
Możesz zobaczyć, czym różni się od C, gdzie składnia jest następująca:
int array[]; // a C array declaration
Wróćmy jeszcze do Javy. Po zadeklarowaniu tablicy należy ją przydzielić:
array = new int[10]; // Java array allocation
Czy można jednocześnie zadeklarować i zainicjować tablicę? Właściwie nie:
int[10] array; // NOPE, ERROR!
Możesz jednak od razu zadeklarować i zainicjować tablicę, jeśli znasz już wartości:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
A co jeśli nie znasz znaczenia? Oto kod, który najczęściej będziesz widzieć podczas deklarowania, przydzielania i używania tablicy int :
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
Zauważ, że podałem tablicę int , która jest tablicą pierwotnych typów danych Java . Zobaczmy, co się stanie, jeśli spróbujesz tego samego procesu z tablicą obiektów Java zamiast prymitywów:
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Jeśli uruchomimy powyższy kod, od razu po próbie użycia pierwszego elementu tablicy otrzymamy wyjątek. Dlaczego? Mimo że tablica jest przydzielona, ​​każdy segment tablicy zawiera puste odniesienia do obiektów. Jeśli wprowadzisz ten kod do swojego IDE, automatycznie wypełni ono za Ciebie plik .val, więc błąd może być mylący. Aby naprawić błąd, wykonaj następujące kroki:
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Ale to nie jest eleganckie. Zastanawiałem się, dlaczego nie mogę łatwo przydzielić tablicy i obiektów w tablicy przy użyciu mniejszej ilości kodu, a może nawet wszystkich w jednej linii. Aby znaleźć odpowiedź, przeprowadziłem kilka eksperymentów.

Znalezienie nirwany wśród tablic Java

Naszym celem jest eleganckie kodowanie. Kierując się zasadami „czystego kodu”, zdecydowałem się stworzyć kod wielokrotnego użytku, aby oczyścić wzorzec alokacji tablicy. Oto pierwsza próba:
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
Linia kodu oznaczona „zobacz to” wygląda dokładnie tak, jak chciałem, dzięki implementacji toArray . Podejście to wykorzystuje odbicie w celu znalezienia domyślnego konstruktora dla podanej klasy, a następnie wywołuje tego konstruktora w celu utworzenia instancji obiektu tej klasy. Proces wywołuje konstruktor raz dla każdego elementu tablicy. Wspaniały! Szkoda tylko, że to nie działa. Kod kompiluje się dobrze, ale po uruchomieniu zgłasza błąd ClassCastException . Aby użyć tego kodu, musisz utworzyć tablicę elementów Object , a następnie rzutować każdy element tablicy na klasę SomeClass w następujący sposób:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
To nie jest eleganckie! Po dalszych eksperymentach opracowałem kilka rozwiązań wykorzystujących refleksję, wyrażenia generyczne i wyrażenia lambda.

Rozwiązanie 1: Użyj refleksji

W tym przypadku używamy klasy java.lang.reflect.Array do utworzenia instancji tablicy określonej klasy, zamiast używać podstawowej klasy java.lang.Object . Jest to zasadniczo jednowierszowa zmiana kodu:
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
Możesz zastosować to podejście, aby uzyskać tablicę żądanej klasy, a następnie pracować z nią w następujący sposób:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Chociaż nie jest to wymagana zmiana, druga linia została zmieniona w celu użycia klasy odbicia Array do ustawienia zawartości każdego elementu tablicy. To jest niesamowite! Ale jest jeszcze jeden szczegół, który nie wydaje się całkiem prawidłowy: rzutowanie na SomeClass[] nie wygląda zbyt ładnie. Na szczęście istnieje rozwiązanie w postaci leków generycznych.

Rozwiązanie 2: Użyj leków generycznych

Struktura Collections używa typów ogólnych do wiązania typów i eliminuje rzutowanie na nie w wielu swoich operacjach. Można tu również zastosować określenia generyczne. Weźmy na przykład Java.util.List .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
Trzecia linia powyższego fragmentu wyświetli błąd, chyba że zaktualizujesz pierwszą linię w ten sposób:
List<SomeClass> = new ArrayList();
Ten sam wynik można osiągnąć, używając typów generycznych w klasie MyArray . Oto nowa wersja:
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
Wygląda dobrze. Używając typów generycznych i włączając typ docelowy do deklaracji, typ można wywnioskować w innych operacjach. Dodatkowo kod ten można zredukować do jednej linii, wykonując następujące czynności:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Misja wykonana, prawda? Cóż, nie do końca. Jest to w porządku, jeśli nie obchodzi Cię, który konstruktor klasy wywołasz, ale jeśli chcesz wywołać konkretnego konstruktora, to rozwiązanie nie zadziała. Możesz nadal używać refleksji, aby rozwiązać ten problem, ale wtedy kod stanie się złożony. Na szczęście istnieją wyrażenia lambda, które oferują inne rozwiązanie.

Rozwiązanie 3: Użyj wyrażeń lambda

Przyznam, że wcześniej nie byłem szczególnie podekscytowany wyrażeniami lambda, ale nauczyłem się je doceniać. W szczególności spodobał mi się interfejs java.util.stream.Stream , który obsługuje kolekcje obiektów. Stream pomógł mi osiągnąć nirwanę tablicy Java. Oto moja pierwsza próba użycia lambd:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
Dla łatwiejszego czytania podzieliłem ten kod na trzy linie. Widać, że spełnia wszystkie wymagania: jest prosty i elegancki, tworzy zapełnioną tablicę instancji obiektów i pozwala wywołać konkretnego konstruktora. Zwróć uwagę na parametr metody toArray : SomeClass[]::new . Jest to funkcja generatora używana do alokacji tablicy określonego typu. Jednak w obecnej postaci ten kod ma mały problem: tworzy tablicę o nieskończonym rozmiarze. To nie jest zbyt optymalne. Ale problem można rozwiązać, wywołując metodę limitu :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
Tablica jest teraz ograniczona do 32 elementów. Możesz nawet ustawić określone wartości obiektów dla każdego elementu tablicy, jak pokazano poniżej:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Ten kod demonstruje siłę wyrażeń lambda, ale kod nie jest schludny ani zwarty. Moim zdaniem wywołanie innego konstruktora w celu ustawienia wartości byłoby znacznie lepsze.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Podoba mi się rozwiązanie oparte na wyrażeniu lambda. Jest to idealne rozwiązanie, gdy trzeba wywołać konkretnego konstruktora lub pracować z każdym elementem tablicy. Kiedy potrzebuję czegoś prostszego, zwykle korzystam z rozwiązania opartego na lekach generycznych, ponieważ jest prostsze. Możesz jednak przekonać się sam, że wyrażenia lambda stanowią eleganckie i elastyczne rozwiązanie.

Wniosek

Dziś nauczyliśmy się, jak pracować z deklarowaniem i alokacją tablic elementów pierwotnych, alokacją tablic elementów Object , używaniem refleksji, wyrażeń generycznych i lambda w Javie.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION