C, PHP, VB, .NET

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


* Методи

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

В Java понятията функции и процедури се обединяват под едно общо наименование - "методи". Спрямо езиците C и C++ тук няма особени изненади - методите приемат като входни параметри данни, обработват ги и връщат резултат от конкретен тип. Методите всъщност са основното средство за логическо разделение на програмния код на отделни модули. По този начин се получава и ефективно "повторно използване на код", както и доста по-опростена поддръжка на програмния код, чрез нива на абстракция.

Вече сме се запознали с главния метод за всяка програма - методът main. Засега ще разгледаме само публични (public) и статични (static) методи, какъвто именно е и main. Когато навлезем в територията на класовете ще се запознаем и с други видове методи.

Декларацията на метод следва следната схема:

public static <тип> <име>(<входни параметри>){...}

Ето един пример - елементарен метод, който намира по-голямото от две числа:

  public static int getLargerNum(int a, int b){
    if (a > b) return a;
    else return b;
  }
  public static void main(String[] args) {
    int x = 2;
    int y = 3;
    System.out.println(getLargerNum(x,y));
  }

Тук важат и всички правила познати досега за локални променливи. Ако декларираме променлива от примитивен тип в тялото на метод, то тя е локална за метода, т.е. след приключването на изпълнението на метода тя ще бъде изтрита.

Когато подаваме входни параметри от примитивен тип, то в метода използваме тяхно копие. Ето един пример:

  public static void notWorkingIncrement(int a){
    a++;
  }
  public static void main(String[] args) {
    int x = 2;
    notWorkingIncrement(x);
    System.out.println(x);
  }

Резултатът от изпълнението на програмата ще е числото 2. Това е така, защото сме подали примитивен тип данни и така сме направили познатото от C "предаване на параметър по стойност", т.е. сме копирали данните в нова локална променлива.

В Java всички променливи към методи се подават по стойност. С други думи ние винаги подаваме копие на информацията. За всеки начинаещ програмист на Java в такъв случай остава въпроса "може ли да се предават параметри по адрес?". Отговорът е положителен, но с лека уговорка.

Нека имаме обект от някакъв тип, например Object. Знаем, че за да се създаде такъв обект се използва оператор new:

Object x = new Object();

Нека имаме сега метод, който приема такъв обект като входен параметър и го променя по някакъв начин:

public static void changeObject(Object a){
   a.change();

После в главната програма извикваме "changeObject(x)". Казахме, че в Java параметрите се предават по стойност, т.е. логически погледнато ние би трябвало да сме предали копие на обект "x". В този случай обаче оригиналният обект "x" ще бъде променен. Това е така, защото фактически стойността на променливата "x" се явява адресът, където са записани данните на обекта. Ние наистина предаваме копие на данните, но това е копие на адреса. Така когато работим с данните на обект "a", то ние фактически ще работим с данните на оригиналния обект "x". По този начин в Java се решава проблема породен от липсата на директно използване на указатели.

Ето и един практически пример. Написваме функция, която инкрементира с единица всеки един елемент на масив:

  public static void incrementArr(int[] a){
    for (int i=0; i<a.length; i++){
      a[i]++;
    }
  }
  public static void main(String[] args) {
    int[] arr = {1, 3, 4, 8};
    incrementArr(arr);
    for (int i: arr){
      System.out.print(i+" ");
    }
  }

Понеже променливата "arr" практически пази адреса на масива, то на функцията се предава именно той и от там инкрементацията на елементите на масива се случва върху оригиналните данни. Така резултатът от изпълнението на примера ще бъде "2 4 5 9 ".

В горния пример може да се забележи, че е валидно и предаването на променлив брой параметри. На практика в декларацията на метод "incrementArr" никъде не зададохме реално с каква размерност е масив "a". Така ние можем да подаваме на този метод различни по дължина масиви и той ще работи без проблем с тях.

Без проблем можем да подаваме и два или повече масива като параметър на метод. Това е така, защото фактически масивите в Java са обекти. Така на логически ние не сме реализирали метод с променлив брой параметри, а сме реализирали метод с фиксиран брой елементи от тип масив.

Java обаче позволява и реално подаване на променлив брой параметри. Следния пример показва отпечатване на двойно по-големите числа от подадените като входни данни на метод:

  public static void showDoubledNums(int ... nums){
    for (int i: nums){
      System.out.print((2*i)+" ");
    }
    System.out.println();
  }
  public static void main(String[] args) {
    showDoubledNums(1,2,5);
    showDoubledNums(12,1,14,2,4);
  }

Резултатът от изпълнението на горния пример е:

2 4 10
24 2 28 4 8

Забелязваме, че това е едно мощно средство за реализиране на многофункционални методи. Никой не ни ограничава и в типа данни. Ето как в предишната прехвърляме не числа от тип int, а цели масиви:

  public static void showDoubledNums(int[] ... nums){
    for (int[] arr: nums){
      for(int i: arr){
        System.out.print((2*i)+" ");
      }
      System.out.println();
    }
  }
  public static void main(String[] args) {
    int[] x = {1,2,5};
    int[] y = {12,1,14,2,4};
    showDoubledNums(x);
    showDoubledNums(x,y);
  }

Единственото важно правило при подаването на променлив брой параметри е, че те трябва да са последните в списъка на входните параметри. Например запис като "public static void func(int ... nums; String s){...}" НЕ Е валиден. Винаги променливия брой параметри трябва да бъде накрая на входните параметри, тоест примера трябва да бъде преработен като "public static void func(String s, int ... nums){...}".

Сега нека обърнем внимание на връщаните стойности от методи и по-специално често срещането при учебни задачи връщане на масив. От програмирането на C знаем, че не е възможно връщането на масив като резултат от функция. Това беше така, защото масивът беше обикновена променлива от стека, но ние нямаше как да определим нейната дължина. Затова се налагаше да създаваме структури (struct). При Java това не е проблем, защото масивите са обекти. Връщането на масив като резултат от функция всъщност връща стойността на адрес на масив от данни - по същият начин както коментирахме за входните параметри. Разгледайте следния пример:

  public static int[] getDoubledNums(int[] nums){
    int[] doubledNums = new int[nums.length];
    for(int i=0; i<nums.length; i++){
      doubledNums[i] = 2*nums[i];
    }
    return doubledNums;
  }
  public static void main(String[] args) {
    int[] x = {12,1,14,2,4};
    int[] doubledX = getDoubledNums(x);
    for(int i=0; i<x.length; i++){
      System.out.println("x = "+x[i]+", and 2*x = "+doubledX[i]);
    }
  }

Масивът doubledNums се създава вътре във функцията. Той обаче НЕ е локална променлива, защото е обект (създаден е с оператор new). Така той остава "жив" и след изпълнението на функцията. Операторът return пък връща резултат променливата - doubledNums. Както вече казахме връщания резултат всъщност е стойността на doubledNums, т.е. адреса на данните. Накрая написаното в главния метод main "int[] doubledX = getDoubledNums(x);" е валидно, защото типовете данни съвпадат - doubledX е обект от тип int[], а връщания резултат от функцията е адрес към обект от същия тип.

Накрая ще споменем понятието "полиморфизъм" на методи. Полиморфизъм е свойството на един метод да прави различни операции в зависимост от обекта, който го е извикал. Това се осъществява като дефинираме методи с едни и същи имена, но различни входни параметри. Например следната програма няма да работи:

  public static boolean isLarger(int a, int b){
    if (a > b) return true;
    else return false;
  }
  public static void main(String[] args) {
    int x1 = 2;
    int x2 = 3;
    System.out.println(isLarger(x1,x2));
    double d1 = 5;
    double d2 = 1;
    System.out.println(isLarger(d1,d2));
  }

Проблемът е, че при вторият System.out.println(isLarger(d1,d2) се подават параметри от тип double, а функцията очаква типове int. Затова ще се възползваме от полиморфизма:

  public static boolean isLarger(int a, int b){
    if (a > b) return true;
    else return false;
  }
  public static boolean isLarger(double a, double b){
    if (a > b) return true;
    else return false;
  }  
  public static void main(String[] args) {
    int x1 = 2;
    int x2 = 3;
    System.out.println(isLarger(x1,x2));
    double d1 = 5;
    double d2 = 1;
    System.out.println(isLarger(d1,d2));
  }

Вече програмата ще се изпълни без проблем. Забелязваме едно изключително удобно средство за изпълнение на едни и същи методи работещи с различни типове данни и различни видове обекти. Ако входните параметри са различни, то е възможно и връщаната стойност да е различна. Например:

  public static int getTheLarger(int a, int b){
    if (a > b) return a;
    else return b;
  }
  public static double getTheLarger(double a, double b){
    if (a > b) return a;
    else return b;
  }
  public static void main(String[] args) {
    int x1 = 2;
    int x2 = 3;
    System.out.println(getTheLarger(x1,x2));
    double d1 = 5;
    double d2 = 1;
    System.out.println(getTheLarger(d1,d2));
  }

Единственото и логично обосновано ограничение е, че не трябва да имаме методи с едни и същи имена и входни параметри, но различни връщани стойности. Това е така, понеже програмата няма да знае кой от двата метода да извика.

От тук нататък примерите за писани на функции са огромно множество, но учебните ще са все тривиални. На практика аналогията между C++ и Java, както и техните съществени разлики са важния момент, който трябваше да усвоите. Най-важното е да свикнете с работата с обекти и приемането/връщането на променлива от тип обект като стойност (т.е. нейния адрес). Едно грешно, но все пак работещо улеснение при прехода от C++ към Java е да приемете, че обектите създадени с new винаги се предават по адрес, а променливите от примитивен тип - по стойност. Разликата с казаното по-горе е тънка и на пръв поглед незначителна, но все пак важна и е добре да я осмислите. В следващата статия ще обвържем методите и изключенията и ще видим как един метод може да прехвърля изключения към друг.

 



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

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


*