opublikował: mkczyk, 2014-01-06

Konwersja typów i rzutowanie

Zmienne są przechowywane w pamięci komputera w postaci ciągu bitów. Programowanie to głównie operacje na tych danych, ale to właśnie programista określa co bity mają reprezentować (liczbę całkowitą, zmiennoprzecinkową, literę) - czyli nadaje typy. Często pojawia się jednak konieczność ich zmiany na inne. Czy to konieczność zwiększenia zakresu dla liczby lub jej dokładności, bądź też zamiany na inny obiekt ale z tymi samymi danymi.
Wbrew pozorom nie zawsze jest to takie proste, a często może powodować nieoczekiwane i trudne do znalezienia błędy. W programach finansowych lub tam gdzie precyzja liczb jest bardzo ważna, błędy wynikłe np. ze złego zaokrąglania mogą powodować katastrofalne w skutkach efekty. Początkujący programiści mogą mieć problemy nawet ze znalezieniem odpowiedniego sposobu rzutowania. Postaram się w artykule wyjaśnić większość typów konwersji typów i dużą część podstawowych z tym problemów.


Czym się różni niejawna konwersja od rzutowania?

Konwersja typów czyli po prostu zmiana typu danej zmiennej na inną. Jeśli może to zostać przeprowadzone bezpiecznie, bez utraty informacji (choć z małymi wyjątkami), to kompilator pozwala na konwersję niejawną. Tzn. programista pisząc kod przypisuje zmienne jednego typu (np. int) do innego typu (double).
int a = 5;
double b = a; // konwersja niejawna


Jeśli zaś konwersja powodowałaby utratę jakiś danych, musi być przeprowadzona jawnie (z małymi wyjątkami). Nazywa się to rzutowaniem (ang. cast) typów. Musimy z niego korzystać także przy próbie konwersji zmiennych znaczących co innego, wymagających ustalenia pewnych założeń lub dokładnej uwagi (np. int i boolean - kompilator nie wie czy np. liczba 3 ma być prawdą czy fałszem). Jest to celowe zabezpieczenie Javy, aby nie utracić żadnych informacji. Jeśli możemy sobie na to pozwolić, programista musi wymusić konwersję za pomocą operatora rzutowania (typ)zmienna - dzięki temu poświęci konieczną większą uwagę konwersji i ryzyko popełnienia błędów wtedy bardzo spadnie.
Dla przykładu: mamy zmienną zmiennoprzecinkową typu double równą 1,7. Jeśli przypiszemy ją do zmiennej typu int, kompilator powiadomi nas błędem \\"incompatible types: possible lossy conversion from double to int\\" informującym o możliwej utracie danych.
double a = 1.7;
int b = a; // błąd!
Jeśli nadal chcemy wykonać konwersję musimy użyć operatora rzutowania:
double a = 1.7;
int b = (int)a; // wynik 1
Do zmiennej b została przypisana obcięta (a nie zaokrąglona) wartość liczby a! Jest to po prostu wzięcie części całkowitej liczby (matematyczna funkcja podłoga lub entier).


Rzutowanie może powodować błędy w programie jeśli źle je przeprowadzimy. Często zmienna początkowo została tak dobrana, aby pomieściła liczbę z należną dokładnością. Jeśli ją źle zaokrąglimy, może to spowodować błędne wyniki.


Jeśli jednak chcemy przybliżyć liczbę z matematyczną zasadą (większe lub równe 5 - w górę, mniejsze - w dół), musimy użyć oddzielnej funkcji np. Math.round():
double a = 1.7;
int b = (int)Math.round(a); // wynik 2


Niejawna konwersja liczbowych typów prostych

Konwersja niejawna jest przeprowadzana jeśli zmienną typu mogącego pomieścić mniej (mniejszy zakres, mniejsza precyzja) chcemy przypisać do zmiennej typu mogącego pomieścić więcej (większy zakres, większa dokładność). Zazwyczaj jest to spowodowane przeznaczeniem większej ilości bitów na zmienną. Np. zmienna typu byte zostanie skonwertowana niejawnie na wszystkie pozostałe typy danych: short, int, long, float, double. Zaś zmienna typu double musi zostać jawnie rzutowana na pozostałe typy: byte, short, int, long, float. Zależność tą przedstawia \\"Tabela konwersji niejawnych i rzutowania liczbowych typów prostych\\". Można łatwo zauważyć zależność i dzięki temu łatwiej się nauczyć.

 

\"\\"Tabela\"

Uwaga, są tu jednak pewne wyjątki jeśli używasz typu long lub konwertujesz int na float.
Niejawna konwersja int na float. Wydawać by się mogło, że wszystko jest dobrze, bo kompilator nie pokazuje błędu:
int a = 123456789; // lub typ long da ten sam wynik
float b = a;
System.out.println(String.format(\\"%.0f\\",b)); // wynik 123456792
Jednak wynik jest inny niż mogłoby się wydawać. Owszem, dostaliśmy liczbę tej wielkości którą mieliśmy, ale typ float miał zbyt mało bitów, aby zachować odpowiednią dokładność.
Ten problem występuje w trzech przypadkach (w innych możemy bezpiecznie używać konwersji niejawnej):

  • int -> float
  • long -> float
  • long -> double

 

Normalnie ten problem występuje nie przy konwersji, a przekroczeniu zakresu zmiennych np.
int a = 2147483647; // = Integer.MAX_VALUE
System.out.println(a+1); // wynik -2147483648


Konwersja i rzutowanie char

char na int

Typ int jest zapisywany na 4 bajtach (zakres do 2147483647), a char na 2 bajtach (zakres do 65535). Wydawać by się mogło, że bez problemu char zmieści się w int. Tak też i jest. Po niejawnej konwersji wszystko się zgadza. Częściej jednak używamy innego zastosowania i źle interpretujemy wynik.
Chcemy do zmiennej char wpisać (np. poprzez podanie od użytkownika) jakąś cyfrę np. 9. Następnie chcemy ją zapisać do zmiennej typu int, żeby wykonać jakieś obliczenia. W tym wypadku po niejawnej konwersji nie otrzymamy oczekiwanej wartości 9, tylko 57:
char a = \\'9\\';
int b = a; // źle! wynik 57

Dlaczego tak się stało? Program wykonał zadanie poprawnie, ale nie tak jak tego oczekiwaliśmy. Kod znaku 9 w Unicode (dla początkowych kodów pokrywają się z tabelą ASCII) to 57. Tak więc, program wyświetlił odpowiednią liczbę - po prostu przekonwertował liczbę w zmiennej char na taką samą liczbę w int (a nie jej reprezentację).

Oczywiście czasem chcemy uzyskać taki efekt i jest to pożądane. Ale jeśli chcemy uzyskać efekt jaki oczekiwaliśmy na początku? Wystarczy zauważyć właściwość, że znaki w kodzie ASCII (jak i Unicode) są ułożone po kolei. Cyfry zaczynają się od kodu 48, a kończą na 57. Jeśli od przekonwertowanego inta odejmiemy 48 otrzymamy prawidłowy wynik:
char a = \\'9\\';
int b = a-48; // wynik 9
Jeśli jeszcze zauważymy, że cyfra 0 ma wartość właśnie 48, to możemy po prostu odjąć własnie wartość tego znaku. Dzięki temu dużo łątwiej zapamiętać tą dosyć często wykorzystywaną konwersję:
char a = \\'9\\';
int b = a-\\'0\\'; // wynik 9
Należy tu pamiętać, że podajemy \\'0\\' w apostrofach (znak char, który potem zostanie automatycznie przekonwertowany na int) a nie w cudzysłowach (mielibyśmy wtedy string).


int na char

W tym wypadku niejawna konwersja jest zablokowana (po prostu nie każdy int zmieści się w char). Ale nawet po użyciu operatora rzutowania nie otrzymamy oczekiwanej wartości, tak jak w poprzednim podrozdziale.
int a = 9;
char b = (char)a; // źle! wynik: tabulator
Jeśli za zmienną int podstawimy 9, spróbujemy przekonwertować w ten sposób i wyświetlimy wynik, to na konsoli pojawi się znak tabulatora zamiast oczekiwanej cyfry 9. Do zmiennej char został przypisany znak w kodzie Unicode o tym numerze tzn. w tym wypadku tabulator. Aby to lepiej zrozumieć odsyłam do podrozdziału o konwersji w drugą stronę z char na int.
W tym wypadku taka konwersja jest raczej nieczęsto używana (wartości tylko od 0 do 9 czyli rzadziej używane znaki ASCII).


Są dwa rozwiązania problemu.
Pierwszym z nich jest przekonwertowanie najpierw zmiennej na String (patrz podrozdział konwersji int na String), pobranie pierwszego znaku i przypisanie do zmiennej char:
int a = 9;
char b = Integer.toString(a).charAt(0);
Drugim sposobem jest skorzystanie z własności tabeli kodu Unicode jak w poprzednim podrozdziale. Z tą różnicą, że najpierw dodamy 48, a potem przekonwertujemy ją na char i otrzymamy prawidłowy wynik:
int a = 9;
char b = (char)(a + 48);
Jeśli zapomnimy jaką liczbę mieliśmy dodać, dodajemy po prostu kod liczby 0 w ASCII (czyli 48):
char b = (char)(a + \\'0\\');


Rzutowanie boolean

int na boolean

Java jest tworzona, aby być jak najbardziej bezpieczna i stara się zabezpieczyć przed błędami. Przez to nie możemy skonwertować wartości w int (lub innej liczbowej) na boolean i odwrotnie za pomocą operatora rzutowania.


Musimy sami zaimplementować taką funkcjonalność. Ale najpierw należy przyjąć założenia (m.in. dlatego w Javie standardowe rzutowanie tego typu nie jest dostępne): 0 jest prawdą (true), 1 jest fałszem (false), inne liczby też są fałszem (false). Być może w twoich zastosowaniach trzeba przyjąć inne założenia (np. generować wyjątek jeśli jest podana inna liczba niż 0 lub 1).

 

Zadanie z tymi założeniami realizuje prosty warunek if (jeśli jest 1 to true, w przeciwnym wypadku false). Można go zapisać w postaci skróconego ifa:
int a = 1;
boolean b = a == 1 ? true : false;
Jeśli często korzystamy z takiej konwersji, warto napisać sobie funkcję np.:
public static boolean intNaBoolean(int a) {
    if (a == 1)
        return true;
    return false;
}
Żebyśmy mogli z niej skorzystać w ten sposób:
boolean b = intNaBoolean(a);


char na boolean

Jeśli zaś chcemy przekonwertować char (często np. pobierany z pliku) do boolean, możemy zrobić to bardzo podobnie. Jednak tym razem porównując z kodem 1:
char a = \\'1\\';
boolean b = a == \\'1\\' ? true : false;
Lub za pomocą funkcji wcześniej utworzonej (razem z konwersją na int):
boolean b = intNaBoolean((char)(a - \\'0\\'));

Inne typy na char

W pozostałych przypadkach konwersji do boolean będzie podobnie. Najłatwiej po prostu zmienną przekonwertować na int, a potem już tylko zrobić tak samo jak przy konwersji int na char.


boolean na typy liczbowe

Tu tak samo możemy użyć skróconego infa. Jedynie zamiast zwracać true lub false, zwracamy 1 lub 0:
boolean a = true;
int b = a == true ? 1 : 0;
Także możemy zrobić własną funkcję:
public static int booleanNaInt(boolean a) {
    if(a == true)
        return 1;
    return 0;
}
Wtedy możemy użyć łatwiej:
int b = booleanNaInt(a);


Inne typy na boolean

Tak jak przy rzutowaniu z boolean na inne typy, tak samo i tu możemy skorzystać z podstawowej wiedzy na temat konwersji inta na boolean. Staramy się skonwertować zmienną na int, a potem robimy jak wyżej.


Konwersje String, klasy opakowujące

Każdemu typowi podstawowemu odpowiada typ obiektowy (np. int ma odpowiednik w klasie Integer). Po co to? Zamiast zmiennej przechowującej wartość, mamy zmienną przechowującą referencję do obiektu zawierającą tą wartość. Dzięki temu możemy np. przekazać do funkcji referencje (tam gdzie jest to wymagane) oraz mamy dostęp do wielu metod dostępnych w klasie.


Rzutowanie na String

Właśnie dzięki tym metodom w klasie opakowujących możemy łatwo przekonwertować dowolny obiekt, który ma metodę toString na String.
A dlaczego nie możemy po prostu użyć operatora konwersji?
int a = 123;
String b = (String)a; // błąd!
String to typ obiektowy dziedziczący bezpośrednio po Object. Zaś np. Integer i Double są podklasami klasy Number, dzięki czemu możemy pomiędzy nimi konwertować za pomocą operatora (typ), zaś tu nie.


Przy konwersji np. z int najwygodniej użyć metody toString(), która jest w klasie Integer:
int a = 123; String b = Integer.toString(a);
Lub metody valueOf() z klasy String:
String b = String.valueOf(a);


char na String

Możemy użyć standardowo metody toString() dostępnej w klasie opakowującej typu z którego chcemy konwertować:
char a = \\'a\\';
String b = Character.toString(a);
Lub metody vauleOf() z klasy String:
String b = String.valueOf(a);
Lub tricku programistycznego: String b = \\"\\" + a;
Poprzez konkatenację (dodawanie) przeciążonym operatorem plusa, nastąpiła jakby niejawna konwersja na String. Jest szybkie we wpisaniu, jednak mało czytelne. Pamiętajmy o osobach czytających kod. Lepiej użyć metody, która wyraźnie wskazuje co jest robione.


String na char

Konwersja ze String na char jest trywialna, bo mamy dostępną metodę charAt() w klasie String.
String a = \\"a\\";
char b = a.charAt(0); // pobieramy pierwszy znak
 

Rzutowanie klas opakowujących

 

Typy \\"mniejsze\\" na \\"większe\\"

Rzutować klasy opakowujące możemy w łatwe i na różne sposoby poprzez dostępne metody w SDK.
Poprzez konstruktor:
Integer a = 5;
Double b = new Double(a);
Poprzez operator rzutowania:
Double b = (double)a;
Przez metodę w klasie Integer: Double b = a.doubleValue();
 

Typy \\"większe\\" na \\"mniejsze\\"

W drugą stronę z racji ograniczeń (spowodowanych bezpieczeństwem) mamy mniej możliwości. Wykorzystamy metodę z klasy Double:
Double a = 1.7;
Integer b = a.intValue();
Pamiętać tu należy, że wartość zostanie obcięta, a nie zaokrąglona (dokładnie tak jak w typach prymitywnych - odsyłam do działu o typach podstawowych).


Konwersja i rzutowanie klas

Do przykładu posłużymy się trzema klasami:
class Klasa {}
class Podklasa extends Klasa {}
class Inna {}
Podklasa dziedziczy po Klasa, a więc powinna być tym co Klasa (mieć te same metody, móc zrobić tyle samo co ona) i może mieć dodatkowo coś jeszcze. Dlatego tu nastąpi konwersja niejawna:
Podklasa podklasa = new Podklasa();
Klasa klasa = podklasa;
Jeśli chcielibyśmy zrobić odwrotnie, to kompilator pokaże błąd. A jeśli nadal zastosujemy operator rzutowania, to program wyrzuci wyjątek: Klasa klasa = new Klasa();
Podklasa podklasa = (Podklasa)klasa; // wyrzuci wyjątek!
W przypadku klasy, która nie należy do tej samej hierarchii (nie ma dziedziczenia) błąd pojawi się już na etapie kompilacji, nawet z użyciem operatora rzutowania:
Klasa klasa = new Klasa();
Inna inna = (Inna)klasa;

Podsumowanie rzutowania typów

Moim celem było zebranie w jednym miejscu wszystkich często używanych konwersji w jednym miejscu, aby móc szybko sprawdzić jak rzutować z jednego typu na inny. W tabeli poniżej zamieściłem sposoby konwersji w każdą ze stron prostych typów danych oraz String. Aby sprawdzić odpowiednie rzutowania dla pola liczbowych typów prostych, należy sprawdzić tabelę wyżej.
Najpierw szukamy typu z którego chcemy rzutować po lewej stronie, a następnie szukamy typu na górze, na który chcemy rzutować.

\"\\"Tabela\"

 


Bibliografia

  1. Cay Horstmann, Gary Cornell: Java 2. Podstawy. Helion, 2003.
  2. Oracle and/or its affiliates: Java™ Platform, Standard Edition 7 API Specification (ang.). Oracle, 1993-2013. [dostęp 2014-01-06].
  3. Oracle: Lesson: Numbers and Strings (ang.). W: The Java™ Tutorials [on-line]. Oracle, 1995-2013. [dostęp 2014-01-06].
  4. Krzysztof Barteczko: Typy danych. Operatory i wyrażenia. W: Podstawy programowania w języku Java [on-line]. Polsko - Japońska Wyższa Szkoła Technik Komputerowych, 2008. [dostęp 2014-01-06].
  5. Andrzej Gąsienica-Samek: Wstęp do Javy dla zaawansowanych. [dostęp 2014-01-06].

Autor: Marcin Kowalczyk © 2014, http://marcinkowalczyk.pl/, dla strefainzyniera.pl

 

Zaloguj się aby dodać komentarz