Садржај
2 Класе и објекти
2.1 Основни појмови о класама и објектима
3 Генеричке класе
4 Наслеђивање и полиморфизам
5 Примери пројеката са решењима
5.1 Различита кретања
5.2 Квиз
5.4 Приказ рада алгоритама сортирања

Енкапсулација

У овој лекцији:

  • зашто подаци треба да буду приватни,

  • шта је конструктор и како може да се напише.

У уводном делу су поменути проблеми који могу да настану због употребе глобалних променљивих и њихове потпуне изложености свима који раде на пројекту. Такође је поменуто да је један начин превазилажења, или бар ублажавања овог проблема увођење класа и проглашавање неких поља класе за приватне податке класе. Тиме се ограничава доступност ових података само на методе класе, тј. подаци се енкапсулирају у класу.

Примером који следи бавићемо се у неколико наврата. За почетак, пример нам даје мотивацију за увођење енкапсулације, а касније ћемо да га искористимо за илустровање још неких концепата, као и за упознавање неких особина језика које нам омогућавају да те концепте спроведемо.

Пример – разломци

Нека је потребно обезбедити поређења и основне рачунске операције над рационалним бројевима, у којима се подаци и резултати задају у облику разломка. Логична идеја је да напишемо класу Razlomak, чији објекти ће да представљају рационалне бројеве. Желимо да имамо на располагању и методе Equals и CompareTo, који постоје у многим класама стандардне библиотеке. Уобичајено понашање ових метода је следеће:

  • метод x.Equals(y) враћа логичку вредност, и то true ако су x и y једнаки, а false у супротном;

  • метод x.CompareTo(y) враћа целобројну вредност, и то негативну ако је x мањи од y, нула ако су једнаки, а позитивну ако је x већи од y.

Стога овакво понашање очекујемо и када су x и y објекти класе Razlomak.

Знамо да су разломци \(r_1 = {p_1 \over q_1}\) и \(r_2 = {p_2 \over q_2}\) једнаки ако имају једнаке бројиоце (\(p_1 = p_2\)) и једнаке имениоце (\(q_1 = q_2\)). Међутим, ови разломци могу да буду једнаки и када \(p_1 \neq q_1, p_2 \neq q_2\). На пример, \({1 \over 2} = {3 \over 6}\) мада \(1 \neq 3, 2 \neq 6\). Такође, \({-1 \over 2} = {1 \over -2}\) мада \(-1 \neq 1, 2 \neq -2\).

Провера једнакости би била једноставнија када бисмо могли да наметнемо додатне услове на облик у коме се памти разломак. На пример, када бисмо за сваки разломак \(p \over q\) били сигурни да је нескратив и да важи \(q > 0\), за проверу једнакости би било довољно да проверимо \(p_1 = p_2\) и \(q_1 = q_2\). Осим тога, било би једноставније да се напише и метод CompareTo (избегли бисмо поређење разломака са негативним имениоцима), метод за приказ вредности разломка (минус се не пише испред имениоца), а вероватно и неки други методи.

Сада се поставља питање како да обезбедимо да увек важе услови које желимо да додатно наметнемо (да је разломак нескратив и да је именилац позитиван). Свакако треба спречити корисника класе да директно задаје или мења вредност бројиоца и имениоца било ког разломка. То значи да бројилац и именилац треба да буду приватна поља класе.

Ово јесте добар почетак, али он отвара нека нова питања. Ако напишемо:

public class Razlomak
{
    private int a, b; // razlomak je oblika a/b gde je b > 0
    ...
}

Razlomak r = new Razlomak() { a = 2, b = 3 };

…добићемо већ поменуте синтаксне грешке које се односе на последњу линију кода

Ове грешке нас упозоравају да не можемо да доделимо почетну вредност разломку на овај начин, јер су поља a и b приватна и зато недоступна.

Конструктор

Да бисмо могли да задајемо почетне вредности разломцима, потребан нам је конструктор објеката класе Razlomak, коме ћемо као параметре да проследимо вредности бројиоца и имениоца.

Конструктор је специјалан метод класе, који увек има исто име као и сама класа, а који као резултат враћа нови објекат те класе.

Ако класа у својој дефиницији нема написан конструктор, компајлер јој аутоматски додељује такозвани подразумевани конструктор без параметара.

Захваљујући постојању подразумеваног конструктора, чак и пре него што научимо шта је конструктор (и пре него што напишемо неки), можемо да пишемо:

Razlomak r = new Razlomak();

…што смо често радили за готове класе из библиотеке. Наведени запис је управо позив подразумеваног конструктора класе Razlomak, који је јаван ако постоји (а постоји ако нисмо написали свој конструктор). Подразумевани конструктор увек враћа објекат у коме су сва поља иницијализована на подразумеване вредности (0 за бројеве и карактере, false за логичке вредности, празан стринг са стрингове, null за референциране типове).

У случају да су поља a и b јавна (public), можемо да им доделимо вредности и након позивања подразумеваног конструктора:

Razlomak r = new Razlomak();
r.a = 2;
r.b = 3;

…што је функционално равноправно са раније коришћеним записом

Razlomak r = new Razlomak() { a = 2, b = 3 };

У нашем случају, пошто смо одлучили да поља a и b буду приватна, од подразумеваног конструктора нема много користи, јер немамо начина да накнадно променимо вредности поља a и b. Зато је овде потребно да напишемо свој конструктор:

public class Razlomak
{
    private int a, b; // razlomak je oblika a/b gde je b > 0

    public Razlomak(int p, int q)
    {
        if (q == 0)
        {
            throw new Exception("Imenilac razlomka je 0");
        }

        // obezbeđujemo uslov da je imenilac pozitivan
        if (q < 0)
        {
            p = -p;
            q = -q;
        }

        // obezbeđujemo uslov da je razlomak neskrativ
        a = p;
        b = q;
        Skrati(ref a, ref b);
    }
    //...
}

Пошто конструктор из нашег примера намеравамо да користимо ван класе, он је означен као јаван (public). Приметимо и то да се при писању конструктора не наводи тип враћене вредности, јер се подразумева да је он исти као и назив метода, односно класе.

public Razlomak(int p, int q)
{
    ...
}

Конструктор који сами напишемо зовемо експлицитан конструктор, за разлику од подразумеваног конструктора, који је имплицитан и настаје аутоматски (пише га компајлер).

У свакој класи можемо да напишемо један или више (произвољан број) конструктора.

Конструктор који смо написали прихвата два целобројна параметра, који представљају вредности бројиоца и имениоца новог разломка. Међутим, те вредности нисмо само уписали у одговарајућа поља, него смо наметнули потребне услове, а да при томе нисмо изменили вредност разломка као целине. Конкретније, у конструктору и бројиоцу и имениоцу мењамо знак у случају да је задати именилац негативан, а затим скраћујемо разломак у случају да није већ скраћен. На тај начин као аутори класе имамо пуну контролу над вредностима бројиоца и имениоца, а корисник класе не може да приступи пољима класе и поремети услове које смо им наметнули.

Увођење и одржавање интерних услова које треба да испуњавају сви објекти дате класе је један од важних разлога због којих нам је енкапсулација потребна. У случају класе Razlomak, то је кључни разлог за енкапсулирање интерних података.

Да бисмо и у наставку рада могли да се ослонимо на важење наметнутих услова унутар класе, водићемо рачуна да сви методи које будемо додали у класу Razlomak одржавају наметнуте услове у постојећим објектима, као и да их успостављају при стварању нових објеката.

На тај начин постижемо да наметнути услови стално важе у сваком објекту класе, па такве услове често називамо инваријантом.

Пошто сваки цео број \(n\) може да се посматра као разломак \(n \over 1\), можемо да напишемо и конструктор који има само један целобројни параметар. Тај параметар представља бројилац будућег разломка, чији именилац је 1. У овом случају потребни услови већ важе (разломак је нескратив, а именилац је позитиван), па је конструктор сасвим једноставан:

public Razlomak(int n)
{
    a = n; b = 1;
}

Када енкапсулирањем података и писањем одговарајућих конструктора осигурамо важење наметутих услова (именилац је позитиван, разломак је нескратив), методи Equals и CompareTo са особинама наведеним на почетку примера сада могу да се напишу знатно једноставније него у случају када не би важили ти наметнути услови.

Конкретно, за проверу једнакости два разломка у методу Equals, довољно је проверити једнакост бројилаца и једнакост именилаца, што без важења наметнутих услова нескративости разломака не би било тачно:

// metod Equals izračunava da li je ovaj razlomak jednak razlomku r
public bool Equals(Razlomak r)
{
    return a == r.a && b == r.b;
}

Слично, из услова инваријанте да су имениоци позитивни, следи \(\frac{a_1}{b_1} < \frac{a_2}{b_2} \iff a_1 b_2 < a_2 b_1\), као и \(\frac{a_1}{b_1} > \frac{a_2}{b_2} \iff a_1 b_2 > a_2 b_1\). Зато је израз \(a_1 b_2 - a_2 b_1\) негативан када је \(\frac{a_1}{b_1} < \frac{a_2}{b_2}\), нула када је \(\frac{a_1}{b_1} = \frac{a_2}{b_2}\), а позитиван када је \(\frac{a_1}{b_1} > \frac{a_2}{b_2}\).

// Metod CompareTo vraća ceo broj koji je negativan ako je ovaj ralomak manji od r,
// nula ako je ovaj razlomak jednak r, a pozitivan ako je ovaj ralomak veći od r
public int CompareTo(Razlomak r)
{
    return a * r.b - r.a * b;
}

Када одлучујемо колико и које конструкторе да напишемо, треба да се руководимо стварним потребама за њима. Непотребни и нелогични конструктори могу да збуне корисника класе и наведу га да пише кôд који је мање јасан и тежи за одржавање. Зато треба да настојимо да обезбедимо довољан број конструктора, оправданих семантиком (смислом), који сви успостављају инваријанту, наметнуту на садржај објекта.

На пример, ако бисмо дефинисали класу Prava, која треба да представља праву у равни или простору, конструктор без параметара нема никаквог смисла, јер није јасно како би требало да изгледа „подразумевана права”.

Следећи пример садржи све делове класе Razlomak, које смо до сада разматрали и написали. Програм можете да копирате у своје радно окружење и испробате.

Напомена: Да смо изоставили дефиницију метода Equals, програм би и даље могао да се изврши, али би се понашао другачије.

Када у нашој класи не би био дефинисан метод Equals, користио би се подразумевани истоимени метод који пореди било какве објекте (а не само разломке). Тај метод ради тако што само провери да ли су једнаке адресе објеката који су му прослеђени као параметри. То значи да би резултат извршавања тог, подразумеваног метода Equals за два различита објекта увек био false, јер су адресе тих објеката различите (садржаји објеката се не би ни поредили).

За приватна поља класе, као што су бројилац a и именилац b у нашој класи Razlomak, кажемо да су енкапсулирана (стављена у капсулу) и могу да се користе само унутар те капсуле, тј. класе. Као што смо већ рекли, корисник класе нема начина да приступи приватним деловима класе, па ни да им мења вредности, мада би њему то можда и било згодно у неким ситуацијама.

У делу класе који је до сада написан, корисник може једино да формира објекте класе Razlomak и да их пореди. У наставку ћемо ову класу да дорадимо до пуне функционалности, која подразумева удобно учитавање и исписивање разломака и рачунање са разломцима.

(Created using Swinx, RunestoneComponents and PetljaDoc)
© 2022 Petlja
A- A+