Hermetyzacja w Javie (enkapsulacja) to zasada, według której obiekt sam pilnuje swoich danych: pola są prywatne, a zmienić je można tylko przez metody, które sprawdzają reguły. Dzięki temu nikt z zewnątrz nie wpisze do konta ujemnego salda ani do testu lokatora, który zna tylko jedna klasa.
To siedemnasta lekcja kursu Javy dla testerów. Pokazujemy w niej cztery modyfikatory dostępu w jednej tabeli, klasę z prywatnymi polami i walidacją, pułapkę getterów zwracających listę, rekordy z Javy 16, które robią połowę tej pracy za Was, oraz Page Object z prywatnymi lokatorami. Cały kod skompilowaliśmy poleceniem javac --release 25, a wyniki działania wpisaliśmy w komentarzach.
Spis treści
- Czym jest hermetyzacja w Javie?
- Jakie są modyfikatory dostępu w Javie?
- Co się psuje, gdy pola są publiczne?
- Gettery i settery: kiedy setter, a kiedy metoda z regułą?
- Jak getter może złamać hermetyzację?
- Czy rekordy w Javie zastępują gettery?
- Hermetyzacja w testach: Page Object z prywatnymi lokatorami
- Jakie błędy hermetyzacji widzimy najczęściej?
Czym jest hermetyzacja w Javie?
Hermetyzacja w Javie polega na ukryciu stanu obiektu przed resztą programu i udostępnieniu tylko tych operacji, które są bezpieczne. Klasa decyduje, co widać na zewnątrz, a co jest jej prywatną sprawą. Narzędziem do tego są modyfikatory dostępu: private, protected, public i dostęp domyślny, nazywany pakietowym.
Hermetyzacja to jeden z czterech filarów programowania obiektowego, obok abstrakcji, dziedziczenia i polimorfizmu. Ogólny obraz wszystkich czterech, z różnicą między hermetyzacją a abstrakcją, znajdziecie w lekcji 14. Programowanie obiektowe: klasy i obiekty. Tutaj schodzimy poziom niżej, do praktyki: jak pisać klasy, które bronią się same.
Po co to testerowi? Bo framework testów to też kod, który ktoś utrzymuje przez lata. Z naszego doświadczenia przy wdrożeniach automatyzacji, a w Quality Island napisaliśmy ponad 450 000 testów automatycznych, najdroższe w utrzymaniu nie są trudne testy, tylko te, w których każda klasa może zmienić wszystko w każdej innej klasie.
Hermetyzacja w Javie w liczbach
4
poziomy dostępu do pól i metod
16
wersja Javy, w której rekordy weszły na stałe
2030
do września wsparcie Premier dla Javy 25 LTS
450 000+
testów automatycznych napisanych w Quality Island
Źródła: Java Language Specification SE 25, rozdział 6.6; OpenJDK JEP 395; Oracle Java SE Support Roadmap 2026; dane własne Quality Island 2026.
Jakie są modyfikatory dostępu w Javie?
Modyfikatory dostępu w Javie są cztery, a specyfikacja języka Java SE 25 (rozdział 6.6) opisuje dokładnie, skąd wolno sięgnąć po każdy element. Najłatwiej zapamiętać je w tabeli, od najwęższego do najszerszego. Uwaga na pułapkę, o którą potykają się nawet osoby po kilku latach pracy: protected daje więcej niż dostęp domyślny, bo poza podklasami obejmuje też cały pakiet.
Źródło: Oracle, The Java Tutorials, Controlling Access to Members of a Class; Java Language Specification SE 25, 2025, rozdział 6.6.
Praktyczna reguła, którą stosujemy w naszych projektach: zaczynajcie od private i otwierajcie dostęp dopiero wtedy, gdy macie konkretny powód. Zmiana z prywatnego na publiczny jest łatwa. Zmiana w drugą stronę, gdy z publicznego pola korzysta już 300 testów, oznacza popołudnie z wyszukiwaniem i poprawianiem.
Dostęp pakietowy ma w testach jedno bardzo przyjemne zastosowanie. Dokumentacja JUnit mówi wprost, że klasy i metody testowe nie muszą być publiczne, wystarczy, że nie są prywatne, i zaleca pomijanie słowa public. Dlatego w przykładzie na końcu tej lekcji klasa LogowanieTest nie ma żadnego modyfikatora.
Co się psuje, gdy pola są publiczne?
Publiczne pole to umowa z całym światem: każdy może wpisać do niego, co chce, i nikt tego nie sprawdzi. Poniżej klasa konta w dwóch wersjach. Pierwsza wpuszcza wszystko, druga broni swojego stanu.
KontoPubliczne.java, wersja do unikania
package pl.qi.dane;
public class KontoPubliczne {
public String email;
public int saldo;
}
Konto.java, Java 25
package pl.qi.dane;
public class Konto {
private final String email;
private int saldo;
public Konto(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Niepoprawny e-mail: " + email);
}
this.email = email;
}
public String getEmail() {
return email;
}
public int getSaldo() {
return saldo;
}
public void wplac(int kwota) {
if (kwota <= 0) {
throw new IllegalArgumentException("Kwota musi być dodatnia: " + kwota);
}
saldo += kwota;
}
}
KontoDemo.java, wynik w komentarzach
package pl.qi.dane;
public class KontoDemo {
public static void main(String[] args) {
KontoPubliczne zle = new KontoPubliczne();
zle.email = "bez-malpy";
zle.saldo = -500;
System.out.println(zle.email + " " + zle.saldo); // bez-malpy -500
Konto dobre = new Konto("anna@example.com");
dobre.wplac(200);
System.out.println(dobre.getSaldo()); // 200
try {
dobre.wplac(-500);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // Kwota musi być dodatnia: -500
}
}
}
Zwróćcie uwagę na trzy rzeczy. Pole email jest private final, więc ustawia je tylko konstruktor i tylko raz. Saldo nie ma settera, ma metodę wplac, która odrzuca kwoty ujemne i zerowe. A obiekt z błędnym e-mailem w ogóle nie powstanie, bo konstruktor rzuci wyjątek. Szczegóły o tym, jak konstruktor pilnuje poprawności obiektu, są w lekcji 15. Konstruktory.
Dla testera to dobra wiadomość podwójnie. Po pierwsze, klasa z regułami w środku sama mówi, co trzeba przetestować: wartości graniczne kwoty, e-mail bez znaku @, wartość null. Po drugie, test nie musi sprawdzać, czy ktoś gdzieś nie ustawił salda na minus 500. Bezpośredni zapis do prywatnego pola zatrzyma kompilator, a ujemną kwotę podaną przez metodę odrzuci walidacja w czasie działania programu, i to ją sprawdzacie testem. Publiczne pole to jak zostawienie kluczy w drzwiach z karteczką „proszę nie wchodzić”: działa dokładnie do pierwszego gościa.
Gettery i settery: kiedy setter, a kiedy metoda z regułą?
Getter (akcesor) zwraca wartość prywatnego pola, setter (mutator) ją zmienia. Zgodnie z konwencją JavaBeans nazywają się getEmail() i setEmail(), a dla wartości logicznych isAktywny(). Sama para getter i setter bez żadnej logiki nie daje jednak hermetyzacji, tylko jej pozór: pole jest prywatne, ale każdy może je zmienić na dowolną wartość.
Dlatego zanim dopiszecie setter, zadajcie sobie trzy pytania:
- Czy ta wartość w ogóle powinna się zmieniać po utworzeniu obiektu? Jeśli nie, pole jest
final, a settera nie ma. Tak jest z e-mailem w klasieKonto. - Czy zmiana ma regułę biznesową? Jeśli tak, zamiast
setSaldo(int)piszecie metodę nazwaną od czynności:wplac,wyplac,zablokuj. Reguła mieszka wtedy w jednym miejscu. - Czy getter nie zdradza zbyt wiele? Jeśli zwraca obiekt modyfikowalny, na przykład listę albo datę typu
java.util.Date, ktoś może zmienić stan obiektu bez pytania. O tym w następnej sekcji.
Ten sposób myślenia przyda się Wam od razu w testach. Obiekt z danymi testowymi, który ma tylko gettery i jest tworzony w jednym miejscu, nie zmieni się przypadkiem między krokiem 3 a krokiem 7 testu. Z naszego doświadczenia ze szkoleń, przez które przeszło ponad 2 500 specjalistów, właśnie dane testowe zmieniane w połowie scenariusza są jedną z najczęstszych przyczyn testów, które „czasem przechodzą”.
Jak getter może złamać hermetyzację?
Getter łamie hermetyzację, gdy zwraca referencję do wewnętrznej, modyfikowalnej kolekcji. Pole jest prywatne, ale wywołujący dostaje do ręki ten sam obiekt listy i może go wyczyścić. Naprawa to zwrócenie kopii tylko do odczytu, na przykład przez List.copyOf, dostępne od Javy 10.
Koszyk.java
package pl.qi.dane;
import java.util.ArrayList;
import java.util.List;
public class Koszyk {
private final List<String> produkty = new ArrayList<>();
public void dodaj(String produkt) {
produkty.add(produkt);
}
// Przeciek: zwracamy oryginalną listę, każdy może ją zmienić
public List<String> getProduktyZPrzeciekiem() {
return produkty;
}
// Poprawnie: niemodyfikowalna kopia
public List<String> getProdukty() {
return List.copyOf(produkty);
}
}
KoszykDemo.java, wynik w komentarzach
package pl.qi.dane;
public class KoszykDemo {
public static void main(String[] args) {
Koszyk koszyk = new Koszyk();
koszyk.dodaj("Kubek");
koszyk.getProduktyZPrzeciekiem().clear();
System.out.println(koszyk.getProdukty().size()); // 0, koszyk wyczyszczony z zewnątrz
koszyk.dodaj("Kubek");
try {
koszyk.getProdukty().clear();
} catch (UnsupportedOperationException e) {
System.out.println("Kopia jest tylko do odczytu"); // ten komunikat zobaczycie
}
System.out.println(koszyk.getProdukty().size()); // 1
}
}
Dokumentacja klasy List w Javie 25 opisuje, że listy z List.of i List.copyOf są niemodyfikowalne i każda próba zmiany kończy się wyjątkiem UnsupportedOperationException. To lepsze zachowanie niż ciche przyjęcie zmiany: błąd wychodzi od razu, w miejscu, które go spowodowało.
Wbrew pozorom
W Javie private chroni pole przed innymi klasami, a nie przed innymi obiektami tej samej klasy. Specyfikacja Java SE 25 (rozdział 6.6.1) pozwala na dostęp do składowej prywatnej z dowolnego miejsca w ciele klasy najwyższego poziomu, która ją deklaruje. Gdybyście dopisali do klasy Konto metodę equals, mogłaby ona bez gettera odczytać inne.saldo z drugiego obiektu. Hermetyzacja w Javie działa na poziomie klasy, nie pojedynczego egzemplarza.
Czy rekordy w Javie zastępują gettery?
Dla klas, które tylko przenoszą dane, tak. Rekord (record) wszedł do Javy na stałe w wersji 16, zgodnie z dokumentem OpenJDK JEP 395. Kompilator sam generuje dla każdego składnika pole private final, publiczny akcesor o tej samej nazwie, konstruktor kanoniczny oraz metody equals, hashCode i toString. To hermetyzacja w jednej linijce, bez kilkudziesięciu linii getterów.
Dwie rzeczy warto wiedzieć, zanim zamienicie wszystkie klasy na rekordy. Akcesory nie mają przedrostka get: piszecie uzytkownik.login(), a nie getLogin(). A pola są final, ale obiekt, na który wskazują, już niekoniecznie. Jeśli składnikiem jest lista, zróbcie jej kopię w konstruktorze kompaktowym, tak jak poniżej.
Uzytkownik.java, rekord z walidacją
package pl.qi.dane;
import java.util.List;
public record Uzytkownik(String login, String haslo, List<String> role) {
public Uzytkownik {
if (login == null || login.isBlank()) {
throw new IllegalArgumentException("Login nie może być pusty");
}
role = List.copyOf(role);
}
public static Uzytkownik administrator() {
return new Uzytkownik("admin.test", "Haslo123!", List.of("ADMIN"));
}
}
UzytkownikDemo.java, wynik w komentarzach
package pl.qi.dane;
import java.util.ArrayList;
import java.util.List;
public class UzytkownikDemo {
public static void main(String[] args) {
List<String> role = new ArrayList<>(List.of("TESTER"));
Uzytkownik u = new Uzytkownik("anna", "Haslo123!", role);
role.add("ADMIN");
System.out.println(u.role()); // [TESTER], zmiana listy z zewnątrz nie przeszła
System.out.println(u.login()); // anna, akcesor bez przedrostka get
System.out.println(u.equals(new Uzytkownik("anna", "Haslo123!", List.of("TESTER")))); // true
}
}
Jedna pułapka przy rekordach z danymi logowania: wygenerowany toString wypisuje wszystkie składniki, także hasło. System.out.println(Uzytkownik.administrator()) pokazuje w logu Uzytkownik[login=admin.test, haslo=Haslo123!, role=[ADMIN]], co sprawdziliśmy w JDK 25. Jeśli taki rekord trafia do logów albo raportów z testów, nadpiszcie w nim toString i zamaskujcie hasło, na przykład zwracając haslo=***.
Źródło: OpenJDK, JEP 395: Records, 2021; Oracle, Java SE 25 Language Guide, Record Classes; zestawienie Quality Island.
Hermetyzacja w testach: Page Object z prywatnymi lokatorami
Page Object to klasa, która reprezentuje stronę albo jej fragment i ukrywa przed testami szczegóły HTML. Dokumentacja Selenium opisuje to wprost: Page Object ma być jedynym miejscem, które zna strukturę strony, nie powinien wystawiać swoich wnętrzności i nie powinien robić asercji, bo to zadanie testu. Martin Fowler w tekście „PageObject” z 2013 roku ujmuje to podobnie: obiekt strony udostępnia usługi, z których korzysta test, a nie elementy strony.
To jest hermetyzacja w czystej postaci. Lokatory są private static final, metoda pomocnicza wpiszDane jest prywatna, a konstruktor PanelKlienta ma dostęp pakietowy, więc panel może utworzyć tylko klasa z tego samego pakietu, po udanym logowaniu. Kod skompilowaliśmy z Selenium 4.
StronaLogowania.java, Selenium 4
package pl.qi.strony;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class StronaLogowania {
private static final By POLE_LOGIN = By.id("username");
private static final By POLE_HASLO = By.id("password");
private static final By PRZYCISK_ZALOGUJ = By.cssSelector("button[type='submit']");
private static final By KOMUNIKAT_BLEDU = By.cssSelector("[data-testid='login-error']");
private final WebDriver driver;
public StronaLogowania(WebDriver driver) {
this.driver = driver;
}
public PanelKlienta zalogujJako(String login, String haslo) {
wpiszDane(login, haslo);
return new PanelKlienta(driver);
}
public StronaLogowania zalogujZBledem(String login, String haslo) {
wpiszDane(login, haslo);
return this;
}
public String komunikatBledu() {
return driver.findElement(KOMUNIKAT_BLEDU).getText();
}
private void wpiszDane(String login, String haslo) {
driver.findElement(POLE_LOGIN).clear();
driver.findElement(POLE_LOGIN).sendKeys(login);
driver.findElement(POLE_HASLO).sendKeys(haslo);
driver.findElement(PRZYCISK_ZALOGUJ).click();
}
}
PanelKlienta.java
package pl.qi.strony;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class PanelKlienta {
private static final By POWITANIE = By.cssSelector("[data-testid='welcome']");
private final WebDriver driver;
PanelKlienta(WebDriver driver) {
this.driver = driver;
}
public String powitanie() {
return driver.findElement(POWITANIE).getText();
}
}
LogowanieTest.java, JUnit Jupiter
package pl.qi.testy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import pl.qi.dane.Uzytkownik;
import pl.qi.strony.StronaLogowania;
class LogowanieTest {
private WebDriver driver;
@BeforeEach
void otworzStrone() {
driver = new ChromeDriver(); // Selenium Manager sam pobierze sterownik
driver.get("https://sklep-demo.example/logowanie");
}
@Test
void administratorWidziPowitanie() {
Uzytkownik admin = Uzytkownik.administrator();
String powitanie = new StronaLogowania(driver)
.zalogujJako(admin.login(), admin.haslo())
.powitanie();
assertEquals("Witaj, admin.test", powitanie);
}
@Test
void zleHasloPokazujeKomunikat() {
String komunikat = new StronaLogowania(driver)
.zalogujZBledem("admin.test", "zle-haslo")
.komunikatBledu();
assertEquals("Nieprawidłowy login lub hasło", komunikat);
}
@AfterEach
void zamknij() {
driver.quit();
}
}
Zobaczcie, czego w teście nie ma: ani jednego By.id, ani jednego selektora CSS. Gdy zespół frontendu zmieni identyfikator pola z username na login, poprawiacie jedną linijkę w StronaLogowania, a nie 40 testów. W naszych projektach automatyzacji to właśnie ta jedna decyzja, prywatne lokatory w obiektach stron, najbardziej obniża koszt utrzymania testów UI. Więcej wzorców, które działają razem z Page Object, opisujemy w artykule o wzorcach projektowych w automatyzacji testów Selenium WebDriver, a dobór samych lokatorów w tekście selektory w Selenium.
Jeśli chcecie zbudować taki framework na szkoleniu z trenerem, zobaczcie szkolenie z automatyzacji testów w Selenium. Gdy wolicie, żeby zrobił to za Was zespół z doświadczeniem, zakres i ceny pokazuje strona automatyzacji testów UI.
Jakie błędy hermetyzacji widzimy najczęściej?
Przeglądając frameworki testów u klientów, widzimy wciąż te same pięć błędów. Każdy ma prostą poprawkę:
- Publiczne pola w klasach z danymi testowymi. Poprawka: rekord albo pola
private finalz konstruktorem. - Getter i setter do każdego pola, generowany odruchowo przez IDE. Poprawka: setter tylko tam, gdzie zmiana ma sens, a zamiast niego metoda nazwana od czynności.
- Getter zwracający wewnętrzną listę. Poprawka:
List.copyOfalbo kopia w konstruktorze rekordu. - Lokatory publiczne albo w klasie testowej. Poprawka:
private static final Byw obiekcie strony. - Asercje w Page Object. Poprawka: obiekt strony zwraca dane, test je sprawdza, tak jak zaleca dokumentacja Selenium.
Pozostałe antywzorce, które psują testy szybciej niż zmiany w aplikacji, zebrała Strefa QA w tekście o najczęstszych antywzorcach w testowaniu oprogramowania. W kolejnej lekcji, 18. Dziedziczenie, zobaczycie, dlaczego protected trzeba używać ostrożnie. Jeśli wolicie przerobić cały materiał z trenerem, mamy szkolenia Programowanie Java dla testerów i Programowanie obiektowe w Java, a osobom, które zaczynają od zera, polecamy wprowadzenie do programowania od podstaw w Javie.
Co zabrać z tego artykułu
01Hermetyzacja w Javie to prywatny stan i publiczne operacje, które pilnują reguł. Sama para getter i setter jej nie daje.
02Cztery modyfikatory dostępu od najwęższego: private, pakietowy, protected, public. Protected obejmuje też cały pakiet.
03Getter zwracający wewnętrzną listę łamie hermetyzację. Zwracajcie List.copyOf.
04Rekordy (od Javy 16) to najlepszy wybór na dane testowe. Listy kopiujcie w konstruktorze kompaktowym.
05W Page Object lokatory są prywatne, asercje zostają w teście. Zmiana selektora to wtedy jedna linijka.
Wasze testy UI pękają przy każdej zmianie w interfejsie? Zbudujemy albo uporządkujemy framework z obiektami stron, które chronią lokatory i skracają utrzymanie.
Powiązane na blogu Quality Island
- 14. Programowanie obiektowe: klasy i obiekty
- 15. Konstruktory
- 16. Funkcje
- 18. Dziedziczenie
- Java dla testerów automatyzujących: jak zacząć naukę
Pełna lista źródeł
- Oracle, The Java Language Specification, Java SE 25 Edition, rozdział 6.6: Access Control, 2025. Stąd poziomy dostępu i reguła dostępu do składowych prywatnych w ciele klasy.
- Oracle, The Java Tutorials: Controlling Access to Members of a Class. Stąd tabela widoczności modyfikatorów.
- OpenJDK, JEP 395: Records, 2021. Stąd wersja 16 i lista składowych generowanych dla rekordu.
- Oracle, Java SE 25 Language Guide: Record Classes, 2025.
- Oracle, Java SE 25 API: interfejs List, listy niemodyfikowalne, 2025.
- Oracle, Oracle Java SE Support Roadmap, stan na 10.10.2026. Stąd wsparcie Premier dla Javy 25 do września 2030.
- Selenium, Page object models, dokumentacja Selenium, 2026.
- Martin Fowler, PageObject, 2013.
- JUnit, Test Classes and Methods, widoczność klas i metod testowych, 2026.
- Dane własne Quality Island, 2026: ponad 450 000 napisanych testów automatycznych, ponad 2 500 przeszkolonych specjalistów.



