Funkcja, ogólnie rzecz biorąc, jest to kawałek kodu przypisany do jakiejś nazwy. Gdziekolwiek w kodzie będzie napotkany ciąg znaków a bezpośrednio po nim para nawiasów okrągłych, kompilator Javy uzna że to jest funkcja i będzie próbował ją wywołać. Funkcje mogą mieć własny zestaw funkcji, niedostępny z zewnątrz, a więc mogą być ściśle wyspecjalizowane do jakiejś czynności, np. opisywana dalej funkcja suma jako parametr będzie dostawała tablicę liczb całkowitych, a zwracać będzie już tylko jedną liczbę całkowitą – sumę wszystkich elementów tablicy podanej jako parametr.
Już w międzyczasie używaliśmy kilku funkcji, ale dotychczas były to funkcje wbudowane w Javę, jak np. funkcja length() stringów, czy funkcja Math.random(). Część z tych funkcji zwracała jakieś wartości, czyli tak jakby w kodzie wywołanie funkcji było zamieniane na wartość, jaką ona zwraca, np. w tym fragmencie:
String str = "test"; int dlugosc = str.length();
funkcja length() stringa str zamieniana jest na wartość jaką zwraca, czyli w
tym przypadku 4, która to jest przypisywana do zmiennej dlugosc. Czyli
ten kod będzie znaczył to samo co:
String str = "test"; int dlugosc = 4;
Ale funkcja nie musi zwracać żadnej wartości. Takie funkcje zazwyczaj wyświetlają coś na ekran lub wykonują jakieś operacje na podanych zmiennych (ale ich nie zwracają, tylko zmieniają te podane), ale to innym razem.
Java jest językiem bardzo obiektowym – w zasadzie wszystko w nim jest obiektem. Polega to na tym, że kod jest podzielony na klasy – w największym skrócie klasę można określić jako zbiór zmiennych i funkcje służące do operacji na nich (w praktyce wychodzi to tak, że albo jednych albo drugich może nie być). Należy rozróżnić pojęcie klasy od pojęcia obiektu. Klasa jest to niejako typ obiektu, a obiekt jest to konkretny egzemplarz, czyli np. Leon jest obiektem klasy człowiek (ale to nie zostało jeszcze potwierdzone).
Taki krótki wstęp był potrzebny żeby móc uzasadnić wstawienie definicji funkcji w odpowiednie miejsce w kodzie. Otóż musi ona być umieszczona wewnątrz klasy (w naszym przypadku jest to klasa Program, bo jak na razie zajmujemy się tylko przypadkiem z tylko jednym obiektem), ale nie wewnątrz innej funkcji. Schematycznie wygląda to tak:
package jbPack; import javax.swing.*; import java.util.*; import java.io.*; public class Program { // Gdzieś tu muszą być funkcje public static void main(String[] args) { new Program(args); // Tutaj nie } // Ale tutaj tak }
Jak już wiadomo (mam nadzieję) co to jest funkcja, to można przystąpić do opisania jak funkcję się definiuje. Definicja określa kilka bardzo ważnych dla funkcji rzeczy:
- typ zwracanej wartości (lub void jeśli nie zwracana jest żadna wartość)
- nazwę funkcji, co by można było ją jakoś wywołać
- ewentualne argumenty (może ich w ogóle nie być, może być konkretna liczba argumentów)
- no i last but not least, ciało funkcji, czyli sam kod który będzie wykonywała funkcja
Wygląda to mniej więcej tak:
zwracany_typ nazwa( parametry ){ // jakiś kod }
Ale lepiej jest wytłumaczyć to na jakimś przykładzie. Napiszmy powiedzmy funkcję o nazwie dodaj(), która będzie przyjmowała dwa argument całkowite i zwracała też liczbę całkowitą, która będzie sumą tych dwóch argumentów. Będzie to wyglądać mniej więcej tak:
int dodaj( int l1, int l2 ){ int wynik; wynik = l1 + l2; return wynik; }
Jak widać, zwracany jest typ int, funkcja przyjmuje 2 parametry typu int: l1 i l2 – w liście parametrów przy definicji funkcji kolejne parametry oddziela się przecinkami. Wewnątrz funkcji tworzona jest nowa zmienna wynik, której przypisywana jest suma argumentów funkcji. Bardzo ważne jest słowo kluczowe return. Oznacza ono zmienną bądź wartość, która będzie zwrócona przez funkcję. Jeśli funkcja ma coś zwracać, to funkcja musi zawierać taką instrukcję. Trzeba też pamiętać, żeby kod mógł dojść do tego miejsca – chodzi mi o to, że jeśli instrukcję return umieścimy wewnątrz instrukcji warunkowej, to jeśli warunek nie będzie spełniony, to instrukcja return nie będzie wykonana, a tak być nie może. Jeśli return jest w instrukcji warunkowej, to musi on być też wewnątrz bloku else, czyli na przykład:
if(liczba == 1) return 10; else return 0;
Ale funkcję dodaj() można zapisać prościej – wystarczy tak:
int dodaj( int l1, int l2 ){ return l1 + l2; }
Nie trzeba definiować żadnych zmiennych pomocniczych. No ale jak z tej funkcji skorzystać? Wystarczy napisać takie wywołanie funkcji:
int wynik; wynik = dodaj(10, 20);
Ale wynik działania tej funkcji nie musi być zachowywany w zmiennej – można go przenieść od razu do innej funkcji, tak że jedna funkcja staje się argumentem innej. Na przykład można od razu wyświetlić wynik dodawania przez wstawienie funkcji dodaj() do funkcji System.out.println():
System.out.println(dodaj(10, 20));
Jako argument w wywołaniu funcji można podać wszystko co ma wartość odpowiedniego tyou, czyli na przykład wynik funkcji, zmienna lub konkretna wartość, np 10 dla typu int.
Trzeba pamiętać, że jeśli parametry nie są tablicami ani żadnymi innymi typami złożonymi, to możemy mieć pewność, że te zmienne które przekazaliśmy jako parametry nie są tymi samymi zmiennymi co wewnątrz funkcji. Fakt, mają one tą samą wartość, ale zmiana jednych nie powoduje zmiany drugich. Przykład – mamy funkcję:
void testowa( int arg ){ arg += 1; }
Jak widać funkcja nic nie zwraca (jako zwracany typ podany jest void) i przyjmuje jeden argument całkowity. Teraz jak gdzieś w kodzie zrobimy coś takiego:
int wartosc = 10; System.out.println( wartosc ); testowa( wartosc ); System.out.println( wartosc );
To dwa razy wyświetli się 10. Dlaczego? Przecież funkcja testowa zmienia swój argument?!? Fakt, zmienia swój argument, akurat o tej samej wartości co zmienna wartosc, a nie zmienną wartosc. Chodzi o to, że przy wywołaniu funkcji z jakimiś argumentami, tworzone są lokalne kopie tych zmiennych, dostępne tylko i wyłącznie w tej funkcji, nie mające wpływu na to co jest na zewnątrz (można wymusić takie 'sprzężenie’ zmiennych zewnętrznych i wewnętrznych, ale tego przypadku teraz nie rozpatrujemy).
Powyżej są podstawy teoretyczne. Teraz czas na praktykę. Zadanie jest takie: „stwórz funkcję generuj(), która nie przyjmuje żadnych argumentów, a zwraca dwuwymiarową tablicę liczb całkowitych o wymiarach 10 na 10 elementów, wypełnioną losowymi elementami z przedziału 0 – 9”. Jak się do tego zabrać? Na tym poziomie stworzenie tablicy z losowymi elementami o podanych wymiarach nie powinno sprawić problemu, ale dla pewności napiszę.
Najpierw tworzymy tablicę o wymiarach 10 na 10:
int[][] vec = new int[10][10];
Teraz trzeba ją wypełnić. Tablica ma wymiary 10×10, ale elementy od 0 do 9, więc trzeba napisać odpowiednio zagnieżdżone 2 pętle for:
for(int i = 0; i < 10; i++){ for(int j = 0; j < 10; j++){
I co tu wstawić? Zmienne i i j będą kolejno przyjmowały wartości od 0 do 9, a więc jeśli w tej pętli można wykorzystać zmienną vec[i][j] i można być pewnym, że po wykonaniu tych pętli, jakakolwiek operacja na tej zmiennej będzie dotyczyła wszystkich elementów tablicy. Ale co do tej zmiennej przypisać? Trzeba użyć funkcji Math.random(). Ale ona zwraca liczby z przedziału 0 – 1, a do tego liczby rzeczywiste, co już zupełnie nam nie pasuje. Najpierw trzeba pomnożyć wynik tej funkcji przez zakres – w naszym przypadku będzie to 10. Jeśli wynikiem funkcji będzie zero, to wynikiem mnożenia też będzie zero. Jeśli wynikiem będzie jeden, to wynikiem mnożenia będzie 10. Jeśli natomiast wynikiem losowania będzie liczba z przedziału 0 – 1, to wynikiem mnożenia będzie liczba z przedziału 0 – 10. No ale to będzie dalej liczba rzeczywista. Wynik naszego mnożenia Math.random() * 10 trzeba zamienić na liczbę całkowitą. Jeśli tego nie zrobimy, to Java będzie się czepiać, że może nastąpić utrata precyzji (czyli utrata tych wszystkich cyferek po przecinku). Ale jeśli napiszemy to jawnie przez tzw. rzutowanie (podanie nazwy typu, który chcemu uzyskać w nawiasie okrągłym przed wartością), to Java nie będzie miała nic do gadania. A więc losowanie naszej liczby z przedziału 1 – 10 i przypisanie jej do elementu tablicy będzie wyglądało tak:
vec[i][j] = (int)(Math.random() * 10);
No więc teraz można poskładać w całość tworzenie tablicy i losowanie jej zawartości (to chyba każdy w tym momencie będzie potrafił zrobić sam). Ale jak narazie to jest zwykły kawałek kodu, a trzeba napisać jakąś definicję funkcji. Trzeba tylko poskładać wszystkie fakty:
- Funkcja ma zwracać tablicę dwuwymiarową, czyli typ int[][]
- Funkcja ma się nazywać generuj
- Funkcja ma nie przyjmować żadnych argumentów
Czyli nagłówek będzie wyglądał tak:
int[][] generuj() {
Ale funkcja zwraca jakąś wartość, a więc trzeba poinformować kompilator, że wynikiem działania funkcji generuj() będzie tablica vec. Jak to zrobić? Przy pomocy instrukcji return. A więc cała funkcja będzie wyglądała tak:
int[][] generuj(){ int[][] vec = new int[10][10]; for(int i = 0; i < 10; i++){ for(int j = 0; j < 10; j++){ vec[i][j] = (int)(Math.random() * 10); } } return vec; }
Funkcja jest już gotowa, ale jak jej użyć? Proste.
int[][] wylosowanaTablica = null; wylosowanaTablica = generuj();
Zadanie domowe:
- Jak przerobić tą funkcję, żeby pobierała jako parametr wielkość tej tablicy
jaką ma wygenerować? - Jak przerobić tą funkcję, żeby pobierała jako parametr zakres z jakiego
będą wylosowane elementy tablicy?
Teraz czas na coś ambitniejszego, czego jeszcze chyba nie było przed funkcjami. Otóż trzeba znaleźć największą wartość w tablicy dwuwymiarowej. Najważniejszy w tym przypadku jest pomysł (ale że ten przykład jest strasznie wyeksplowatowany, to wystarczy lektura kilku książek o programowaniu). Trzeba wprowadzić jakąś zmienną pomocniczą, w której będzie zachowana tymczasowa największa wartość – na początek będzie to zero, a później jeśłi, znajdzie się jakiś większy element tablicy, będzie odpowiedno zmieniana. Teraz pewne założenia co do funkcji. Co o niej wiemy?
- Ma się nazywać max
- Ma przyjmować jako parametr tablicę dwywymiarową
- Ma zwracać pojedyńczą liczbę całkowitą
A więc początek definicji będzie wyglądał tak:
int max( int[][] vec ){
No i teraz trzeba jakoś to sprawdzić. Nie znamy wielkości tablicy vec, nie wiem czy jest prostokątna czy szarpana, więc do pętli for trzeba będzie wykorzystać zmienną vec.length dla ilości wierszy i vec[i].length dla długości każdego wiersza. No więc na początek trzeba zainicjować zmienną temp:
int temp = 0;
(Uwaga: 0 jest tu prawidłowe tylko jeśli tablica zawiera tylko liczby dodatnie). Dalej standardowa pętla przelatująca całą tablicę:
for(int i = 0; i < vec.length; i++){ for(int j = 0; j < vec[i].length; j++){
No i w tym miejscu trzeba wstawić jakiś sprytny warunek – słownie to będzie tak: jeśli aktualnie sprawdzany element jest większy od tego co jest w zmiennej temp, no to wiadomo, że to co jest w temp nie jest największe, bo to co jest w tym elemencie jest większe, a więc tymczasowo można przyjąć że to co jest w tym elemencie jest największe i przypisać to do temp. A w Javie będzie to wyglądało następująco:
if(vec[i][j] > temp) temp = vec[i][j];
No i po przewianiu całej tablicy mamy pewność, że to co jest w temp jest największą wartością występującą w tablicy. Teraz trzeba tylko zwrócić wynik działania naszej funkcji. A więc cała funkcja będzie wyglądała tak:
int max( int[][] vec ){ int temp = 0; for(int i = 0; i < vec.length; i++){ for(int j = 0; j < vec[i].length; j++){ if(vec[i][j] > temp) temp = vec[i][j]; } } return temp; }
Zadanie domowe: jak należałoby zainicjować zmienną temp żeby ta funkcja działała także dla liczb ujemnych?
Funkcja suma(), którą też trzeba napisać, jest prawie identyczna jak poprzednia. Ma ona zwrócić sumę wszystkich elementów w tablicy dwuwymiarowej podanej jako argument funkcji. Czyli co trzeba zrobić? Trzeba poprostu każdy element dodać do jakiejś zmiennej tymczasowej. Czyli w poprzedniej funkcji wystarczy zamiast warunku i zmiany wartości temp napisać:
temp += vec[i][j];
No i oczywiście przydałoby się zmienić nazwę tej funkcji. Reszta pozostaje taka sama, więc nie będę się rozpisywał.
Kolejna funkcja do napisania to ileParzystych(). Także ona ma dostawać tablicę dwuwymiarową jako argument i ma zwrócić ilość elementów w tej tabeli, które są parzyste. No i jak to zrobić? Potrzebny jest nam jakiś licznik, co by notować ile już naliczyliśmy tych parzystych elementów – oczywiście na początku powinien mieć on wartość 0. No i teraz trzeba sprawdzić każdy element. Jeśli jest parzysty – to zwiększamy licznik. Na końcu zwracamy licznik. Tutaj też można wykorzystać jako podstawę funkcję max(), bo ona też ma zwracać int’a, dostawać dwuwymiarową tablicę intów, potrzebuje zmienną tymczasową i na każdym elemencie tablicy sprawdza jakiś warunek i coś robi w razie spełnienia tego warunku. Jak ma wyglądać ten warunek? Mniej więcej tak:
if(vec[i][j]%2 == 0) temp++;
Czyli jeśli reszta z dzielenia jest równa 0 (czyli jeśli liczba jest podzielna przez 2, czyli liczba jest parzysta) nasza zmienna tymczasowa temp która w tej chwili jest traktowany jako licznik jest zwiększana o jeden. No i po całej pętli w zmiennej temp będzie się znajdowała liczba parzystych elementów tablicy, którą trzeba zwrócić. I to jest cała filozofia.
Ostatnią funkcją do przerobienia jest funkcja reverse(). Jest ona o tyle inna od poprzednich, że operuje na stringach – pobiera string i zwraca string. Ma ona string podany jako argument zwrócić, ale w postaci 'od końca’. Początek będzie więc wyglądał tak:
String reverse(String str) {
To chyba nie jest problem. Trzeba sobie stworzyć jakiegoś tymczasowego stringa,
np. out :
String out = "";
I teraz trzeba sobie napisać ładną pętlę for, która będzie przerabiała wejściowego stringa od końca. Wartości, jakie będzie przyjmował parametr pętli for, będą musiały być przekazane do funkcji charAt(), więc muszą się zawierać w przedziale od 0 do (długość stringa – 1) ponieważ znaki w stringu numerowane są od zera. Tylko że trzeba będzie to zrobić od końca, czyli najpierw będzie (długość stringa – 1) a na końcu 0. Pętla będzie wyglądałą tak:
for(int i = str.length() - 1; i >= 0; i--){
(i musi być >= 0, bo musi też przyjąć wartość 0). No i teraz trzeba kolejno każdy znaczek ze stringa wejściowego str dopisać na koniec stringa wyjściowego out:
out += str.charAt(i);
I po całej pętli trzeba zwrócić stringa wyjściowego. Cała funkcja:
String reverse(String str) { String out = ""; for(int i = str.length() - 1; i >= 0; i--){ out += str.charAt(i); } return out; }
Te funkcje w Javie nazywaja sie metdowami o ile mnie pamiec nie myli 🙂
Funkcje metodowe? Nie słyszałem o czymś takim… Gógle też nic nie mówi na ten temat.