Interfejsy

Prawdopodobnie interfejsy kojarzą Ci się głównie z interfejsem graficznym tzw. GUI – Graphical User Interface. Jednak nie dziś o tym, porozmawiamy – poruszmy kwestwię interfejsów programowania obiektowego w Javie.

Czym są te interfejsy?

Interfejs można utożsamiać z osobą, która nakazuję drugiej osobie – np. uczniowi wykonać jedną lub wiele czynność. Takiego nauczyciela nie interesuje jak uczeń wykona to zadanie – dla niego się liczy, że ma to zrobić. No to własnie tak z grubsza możemy rozumieć interfejs. Przejdźmy teraz do konkretów.

Interfejs jest zbiorem metod, które klasa musi wykonywać (musi w sobie zaimplementować), lecz interfejs zbytnio nie obchodzi w jaki sposób dana klasa to wykona. Trochę już się Ci to przybliża?

Dobrym przykładem interfejsu może być zbiór metod jakiegokolwiek pojazdu – przypuśćmy, że każdy pojazd jeździ i hamuję. Więc teraz każda klasa, która implementuje ten interfejs musi mieć metodę od jeżdzenia i hamowania. Czy to będzie rower, samolot, czy pociąg – to wszystkie te pojazdy mają wykonywać to samo – tylko, że w inny sposób – sam sposób (implementacja) siedzi w konkretnej klasie.

Mamy już trochę teorii – stwórzmy w końcu pierwszy interfejs w Javie.

Pierwszy interfejs

Interfejs tworzymy podobnie tak jak klasę w Javie – tylko zamiast Class wybieramy Interface 

Niech będzie to interfejs Vehicle, która będzie zawierał w sobie metody, które ma mieć każdy pojazd!

Interfejsy można grupować również w osobny pakiet – ja zazwyczaj nazywam go API – Abstract Programming Interface – czyli własnie abstrakcyjne interfejsy, które tylko mówią co można wykonać, a nie jak. 😉

Skoro mamy już stworzony interfejs, która wygląda tak:

package com.company.api;

public interface Vehicle {
    
}

Spójrz, że zamiast słowa class pojawia się słowo interface.

To czas na zdefiniowanie pierwszych metod – podkreślam, że bez implementacji! Od implementacji służy klasa!

Mamy metodę drive, która definiuje jazdę pojazdu, i stop, która mówi o zatrzymaniu pojazdu.

package com.company.api;

public interface Vehicle {
    public void drive();
    public void stop();
}

I jak widzisz brak żadnej implementacji – co teraz?

Możemy teraz stworzyć klasę, która go zaimplementuję (czyli będzie musiała zaimplementować wszystkie jego metody!)

Choć jest wyjątek – gdy klasa jest abstrackcyjna czyli public abstract class Klasa nie musi ona implementować żadnej z tych metod, jednak o tym kiedy indziej. Teraz skupimy się na zwykłych klasach, które musza implementować interfejsy.

Niech będze to klasa Bike

package com.company;

import com.company.api.Vehicle;

public class Bike implements Vehicle {
    
}

Jak widzisz interfejsy są implementowane przy użyciu słówka implements – teraz musimy zaimplementować obie metody z interfejsu, niech będa one tylko wypisywać na ekran odpowiedni tekst – zależnie od tego jakie to urządzenie. 😉

package com.company;

import com.company.api.Vehicle;

public class Bike implements Vehicle {

    public Bike() {
        
    }

    @Override
    public void drive() {
        System.out.println("Jade rowerem, więc muszę pedałować!");
    }

    @Override
    public void stop() {
        System.out.println("Hamuję rowerem, musze używać hamulca tylniego, który jest przy mojej prawej dłoni.");
    }
}

Wypisanie takich tekstów powinno pomóc Ci zrozumieć, o co chodzi w różnych implementacjach tych samych interfejsów.

Jeszcze klasa Car, która jeździ i hamuję w inny sposób:

package com.company;

import com.company.api.Vehicle;

public class Car implements Vehicle {
    
    public Car() {
        
    }
    
    @Override
    public void drive() {
        System.out.println("Jade samochodem, więc dodaję gazu i się nie męcze!");
    }

    @Override
    public void stop() {
        System.out.println("Hamuję samochodem naciskając środkowy pedał.");
    }
}

Widzisz, że implementacja samochodu się różni od implementacji roweru.

Skoro mamy już dwie klasy to stwórzmy oba obiekty w metodzie main.

Tworzenie obiektów

W klasie main utwórzmy obiekty i wywołajmy obie metody – drive i stop.

package com.company;

public class Main {

    public static void main(String[] args) {
      Bike bike = new Bike();
      bike.drive();
      bike.stop();

      Car car = new Car();
      car.drive();
      car.stop();
    }
}

Po uruchomieniu otrzymujemy:

Jade rowerem, więc muszę pedałować!
Hamuję rowerem, musze używać hamulca tylniego, który jest przy mojej prawej dłoni.
Jade samochodem, więc dodaję gazu i się nie męcze!
Hamuję samochodem naciskając środkowy pedał.

Skoro i Car i Bike są pojazdami to może da się jeszcze inaczej przypisać utworzone obiekty? Oczywiście, że tak!

Skoro obie klasy implementują ten sam interfejs – to obie klasy są pojazdami – więc możemy je przypisać do typu Vehicle.

import com.company.api.Vehicle;

public class Main {

    public static void main(String[] args) {
      Vehicle bike = new Bike();
      bike.drive();
      bike.stop();

      Vehicle car = new Car();
      car.drive();
      car.stop();
    }
}

Co nam to daje?

Interfejsy pozwalają nam na wyodrębnienie pewnej abstrakcji – abstrakcja mówi nam co można zrobić, jednak nie mówi o tym jak to zrobić.

Przypuśćmy, że mamy klasy odpowiedzialne za relację z bazą danych – każda z klas powinna posiadać możliwość dodawania, edytowania, usuwania oraz wyciągania wartości. Tworząc jeden wspólny interfejs dla wszystkich takich klas mamy pewność, że o żadnej metodzie z żadnej tych klas nie zapomnimy. Po prostu interfejs będzie takim strażnikiem, który nie puści nas bez zaimplementowania jego metod.

To kiedy pisać interfejsy?

Jak już pewnie wyczułeś interfejsy mówią co ma być zrobione – ale nie jak. Dlatego interfejsy mogą być używane do pisania jakiś serwisów, dzięki którym coś robimy. Bez sensu jest pisać interfejs dla prostego modelu – no bo co by w nim umieścić – getLogin()? Takie zastosowanie będzie bez sensu.

Za to lepiej wykorzystać interfejs własnie do serwisów, w nim możemy już z góry zdefiniować za co serwis będzie odpowiedzialny. A później to po prostu zaimplementować i prawda jest taka, że prawdopodobnie tylko jeden serwis będzie implementował ten interfejs. Więc czy tworzenie interfejsu ma sens, gdy implementuję go tylko jedna klasa?

Moim zdaniem tak, po pierwsze to póki co jednak klasa go implementuję, w przyszłości może to się zmieni. Po drugie klasy czasami osiągają duże rozmiary – choć nie powinno się tak dziać to czasami nie ma opcji i musi już tak to być. W takim przypadku przychodzącą nam z pomocą interfejsy, do których możemy zajrzeć i szybciej zobaczyć dostepne metody. W końcu w nich nie ma implementacji – więc nie musimy przewijać X razy klasy, aby przejrzeć wszystkie metody.

Skoro interfejs warto tworzyć dla serwisów to i my stwórzmy sobie taki serwis dla naszej aplikacji.

UserService

Zajmijmy się stworzeniem interfejsu dla UserService – niech interfejs ma nazwę UserService, zaś jego implementację nazwiemy (czyli klasę) UserServiceImpl – co wskazuje, że jest tutaj implementacja.

W UserService będziemy operować obiektem User, który został stworzony na koniec drugiego tygodnia w aplikacji domowej, a rozwiązanie jej zostało wytłumaczone na początku tego tygodnia. Dlatego jeżeli nie wykonałeś tego zadania to natychmiast je wykonaj, ponieważ w tym miejscu bedziemy dalej tworzyć naszą aplikację.

Weźmy się za tworzenie interfejsu UserService – umieśćmy go od razu w pakiecie api.

package api;

public interface UserService {
}

Teraz możemy zdecydować co nasz UserService ma potrafić. Niech będą to operację dodawania, usuwania i wyciągania Usera. UserService póki co będzie to robił przy użyciu Listy (takiej tablicy na sterydach, o której zaraz Ci powiem 😉 ) – jednak w późniejszej części kursu podepniemy pod to klasę odpowiedzialną za przetrzymywanie danych w pliku – powiedzmy, że taka nasza baza danych. 😉

Metoda odpowiedzialna za wyciąganie wszystkich Userów:

List<User> getAllUsers();

Zaraz opowiem Ci co oznacza List<User>

Metoda odpowiedzialna za dodawanie nowego Usera:

void addUser(User user);

Oraz odpowiedzialna za usunięcie usera po jego id:

void removeUserById(Long userId);

Cały nasz interfejs wygląda, więc tak:

package api;

import entity.User;

import java.util.List;

public interface UserService {
    List<User> getAllUsers();
    void addUser(User user);
    void removeUserById(Long userId);
}

I nawet, gdyby nasza implementacja wyglądała bardzo skomplikowanie to zawsze możemy otworzyć interfejs i zerknąć jak ta klasa cała wygląda.

Skoro mamy już interfejs to czas za napisanie do niego testów i dopiero implementacji metod. Jednak jeszcze wcześniej jestem Ci winien wyjaśnienia czym jest Lista.

Cóż to za “List”…

W programowaniu mamy coś takiego jak struktury danych – czyli przeróżne kontenery na dane. Każdy z nich wyróżnia się czymś innym, innym szybkością dodawania elementów i usuwania, inny zaś czasem dostawania się do konkretnego elementu – czyli wyszukiwania.

Struktury danych to tak jak nazwałem tablice na sterydach – czyli struktura danych to nic innego jak klasa, w której zazwyczaj jest znajduje się tablica. Są jeszcze inne implementację, gdzie nie stosuję się tablicy, jednak w tym czase dla Ciebie łatwiej będzie sobie wyobrazić strukturę danych jako tablicę opakowaną w klasę, która zapewnia nam wszystkie metody do operacji na tej wewnętrznej tablicy.

Szybkość konkretnych działań zależy od implementacji – czyli jeżeli dodawanie działa szybciej to znaczy, że zostało to inaczej zaimplementowane niż to, że wyszukiwanie jest szybsze.

Pamiętasz może jak na początku pierwszego tygodnia tworzyliśmy aplikację todo i musieliśmy przesuwać całą tablicę w dół i w górę aby zrobić miejsce lub je zamaskować dla nowego/usuwanego elementu? Tą i inne podobne operację zapewniam nam właśnie struktura danych.

I dochodzimy aż do momentu, gdzie dowiadujemy się, że List – czyli lista elementów jest strukturą danych. W nawiasach ostrych czyli <User> mówimy kompilatorowi jakie obiekty będą tam przechowywane – my chcemy listę userów dlatego mamy List<User>.

Warto też zaznaczyć, że List jest interfejsem – więc nie możemy utworzyć jego instancji. Interfejs może być tylko implementowany, nie może zaś być tworzona jego instancja. Więc w interfejsie List mamy tylko powiedziane co każda lista musi umieć potrafić – jednak, żeby stworzyć instancję listy potrzebujemy już konkretnej implementacji. Tak samo jak było w przypadku Pojazdu – Vehicle mówił nam tylko co pojazd ma robić, a Car i Bike implementowały te zachowania.

Tak samo tutaj – interfejs List mówi co każda lista ma robić, a implementacja jest zawarta w klasie LinkedList oraz ArrayList – my skorzystamy z imlementacji ArrayList, choć moglibysmy też użyć LinkedList.

Tak na boku, więc szybko zróbmy pogląd tego jak używać listy.

Jej inicjalizacja wygląda tak:

List<Integer> numbers = new ArrayList<Integer>();

Czyli mamy już listę Integers – czyli listę liczb. Zwróć uwagę, że w nawiasach ostrych mogą być tylko typy obiektowe, a nie prymitywne! Czyli int nie przejdzie bo jest typem prymitywnym – miej na uwadze, że typy obiektowe to te, które są pisane wielką literą np. String, Integer, prymitywy są pisane z małej litery np. int, float.

Do naszej listy możemy dodawać elementy przy użyciu metody add – ne musimy już odwoływać się do konkretnego indexu tablicy, metoda add wstawi nasz nowy element na sam koniec listy.

numbers.add(5);
numbers.add(7);
numbers.add(3);

Usuwać po indeksie możemy przy użyciu metody remove.

numbers.remove(1);

Liczbę elementów możemy wyciągnąć przy użyciu size();

numbers.size();

Wystarczy Ci tyle wiedzy na temat listy, możesz więcej przeczytać o strukturach danych w moim wpisie o kolekcjach. Są tam dość obszernie opisane podstawowe kolekcje, których się używa podczas programowania wraz z przykładami.

Testy

Czas na napisanie testów, skoro mamy już nasz interfejs. Ze względu, że nasza klasa będzie operować na liście to musimy jej przekazać listę wartości, na której będziemy testować dlatego zrobimy to przy użyciu konstruktora.

Na początek tworzymy pakiet service src/test/java, a w niej nową klasę: UserServiceTest – pamiętaj, aby na końcu pojawiło się słowo Test inaczej Maven nie znajdzie twojego testu podczas uruchamiania life cycle.

package service;

public class UserServiceTest {
}

Test getAllUsers

Dodajemy piewszy test odpowiedzialny za sprawdzenie czy service poprawnie zawraca wszystkich userów.

@Test
public void testGetAllUsers() {
}

Naszymi danymi wejściowymi czyli is jest lista userów:

//is
List<User> users = new ArrayList<User>();
users.add(new User(1l, "admin", "admin"));
users.add(new User(2l, "pablo", "admin"));

Czyli tworzymy w pierwszej lini listę na Userów i dodajemy do niej dwóch nowych użytkowników.

Następnie robimy wywołanie metody getAllUsers() na obiekcie utworzonym przy użyciu listy użytkowników utworzonej wyżej.

//then
UserServiceImpl userService = new UserServiceImpl(users);
List<User> usersFromTestClass = userService.getAllUsers();

I oczekujemy, aby nasza otrzymana lista była równa tej, którą daliśmy w konstruktorze UserServiceImpl.

//expected
Assert.assertEquals(users, usersFromTestClass);

Czyli nasz cały test wygląda tak:

package service;

import entity.User;
import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

public class UserServiceTest {

    @Test
    public void testGetAllUsers() {
        //is
        List<User> users = new ArrayList<User>();
        users.add(new User(1l, "admin", "admin"));
        users.add(new User(2l, "pablo", "admin"));

        //then
        UserServiceImpl userService = new UserServiceImpl(users);
        List<User> usersFromTestClass = userService.getAllUsers();

        //expected
        Assert.assertEquals(users, usersFromTestClass);
    }
}

Teraz napiszemy kolejne testy, a później zajmiemy się implementacją UserService.

Test addUser

Na początku tworzymy listę userów i dodajemy do niego jednego usera.

//is
List<User> users = new ArrayList<User>();
User user = new User(1l, "admin", "admin");
users.add(user);

Kolejno musimy stworzyć obiekt UserServiceImpl jednak zrobimy to przy użyciu konstruktora bezparametrowego – w którym lista userów będzie pusta.

//then
UserServiceImpl userService = new UserServiceImpl();

Czas na dodanie nowego użytkownika:

userService.addUser(user);

I wyciągnięcie wszystkich użytkowników, aby porównywać otrzymaną listę z tą, którą my mamy w swoim teście.

List<User> usersFromTestClass = userService.getAllUsers();

Do obu list został dodany ten sam użytkownik, więc powinny one być równe.

//expected
Assert.assertEquals(users, usersFromTestClass);

Cały test wygląda tak:

@Test
public void testAddUser() {
    //is
    List<User> users = new ArrayList<User>();
    User user = new User(1l, "admin", "admin");
    users.add(user);

    //then
    UserServiceImpl userService = new UserServiceImpl();
    userService.addUser(user);
    List<User> usersFromTestClass = userService.getAllUsers();

    //expected
    Assert.assertEquals(users, usersFromTestClass);
}

Test removeUser

Czas na sprawdzenie czy user usuwa się poprawnie.

Mamy na początku listę dwóch userów:

//is
List<User> users = new ArrayList<User>();
User admin = new User(1l, "admin", "admin");
User pablo = new User(2l, "pablo", "admin");
users.add(admin);
users.add(pablo);

Tworzymy obiekt UserServiceImpl z użyciem tej listy i z obu list usuwamy ten sam obiekt – w UserService jest to obiekt, który ma user id  = 1, zaś w w naszej liście jest to obiekt o nazwie admin.

//then
UserServiceImpl userService = new UserServiceImpl(users);
userService.removeUserById(1l);
users.remove(admin);
List<User> usersFromTestClass = userService.getAllUsers();

I obie listy również powinny być sobie równe, w końcu z ich obu usunęliśmy ten sam obiekt – tylko w inny sposób.

//expected
Assert.assertEquals(users, usersFromTestClass);

Cały test wygląda tak:

@Test
public void testRemoveUser() {
    //is
    List<User> users = new ArrayList<User>();
    User admin = new User(1l, "admin", "admin");
    User pablo = new User(2l, "pablo", "admin");
    users.add(admin);
    users.add(pablo);

    //then
    UserServiceImpl userService = new UserServiceImpl(users);
    userService.removeUserById(1l);
    users.remove(admin);
    List<User> usersFromTestClass = userService.getAllUsers();


    //expected
    Assert.assertEquals(users, usersFromTestClass);
}

Implementacje

Skoro mamy już napisane te trzy testy to czas przejść do implementacji testowanych metod.

Zacznijmy od stworzenia klasy UserServiceImpl w pakiecie service. Klasa musi mieć konstruktor bezparametrowy, w którym będzie inicjalizowana pusta lista: Pamiętaj, że nasza klasa implementuję UserService.

package service;

import entity.User;

import java.util.ArrayList;
import java.util.List;

public class UserServiceImpl implements UserService {

    List<User> users;

    public UserServiceImpl() {
        this.users = new ArrayList<User>();
    }
}

Konstruktor parametrowy, przez który możemy podać już gotową listę użytkowników:

public UserServiceImpl(List<User> users) {
    this.users = users;
}

I możemy wziąć się teraz za pisanie trzech metod: getAllUsers, addUser, removeUser.

getAllUsers()

Metoda jest odpowiedzialna za zwrócenie wszystkich użytkowników – czyli musi zwracać całą listę – jest to tak naprawdę zwykły getter, który potrafisz już tworzyć.

public List<User> getAllUsers() {
    return users;
}

Nic skomplikowanego się nie dzieje – zwracamy listę użytkowników z klasy.

addUser

Metoda jest odpowiedzialna za dodawanie nowego użytkownika do listy – do tego posłużymy się metodą add, którą dostarcza nam lista.

public void addUser(User user) {
    users.add(user);
}

removeUser

Oraz metoda odpowiedzialna za usuwanie użytkownika po jego indexie na liście – skorzystamy z metody removektórą dostarcza nam lista.

Przeszukujemy całą listę przy użyciu pętli for i jeżeli napotkamy na to samo ID to usuwamy Usera z listy i kończymy działanie pętli.

public void removeUserById(Long userId) {
    for(int i=0;i<users.size();i++){
        //wyciągnięcie i-tego usera z listy
        User userFromList = users.get(i);
        //jeżeli ID usera z listy jest równe podanemu userId do usunięcia
        if (userFromList.getId() == userId) {
            //to usuń tego usera z pod i-tego indexu
            users.remove(i);
            // i przerwij pętle w końcu user już został usunięty.
            break;
        }
    }
}

Więc cała nasza klasa prezentuję się tak:

package service;

import api.UserService;
import entity.User;

import java.util.ArrayList;
import java.util.List;

public class UserServiceImpl implements UserService {

    List<User> users;

    public UserServiceImpl() {
        this.users = new ArrayList<User>();
    }

    public UserServiceImpl(List<User> users) {
        this.users = users;
    }

    public List<User> getAllUsers() {
        return users;
    }

    public void addUser(User user) {
        users.add(user);
    }

    public void removeUserById(Long userId) {
        for(int i=0;i<users.size();i++){
            //wyciągnięcie i-tego usera z listy
            User userFromList = users.get(i);
            //jeżeli ID usera z listy jest równe podanemu userId do usunięcia
            if (userFromList.getId() == userId) {
                //to usuń tego usera z pod i-tego indexu
                users.remove(i);
                // i przerwij pętle w końcu user już został usunięty.
                break;
            }
        }
    }

}

Skoro mamy już wszystkie metody to czas na uruchomienie testów przy użyciu Mavena.

mvn install

I jak widać testy się powiodły.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running service.UserServiceTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.095 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

Na koniec warto będzie zapisać nasze zmiany na gicie przy użyciu:

git add *
git commit -m "Impl UserService"
git push origin master

Po wpisaniu tych trzech komend nasze zmiany zostaną wypchnięte na zdalne repozytorium. 😉

Kod utworzonej całej aplikacji do tego momentu możesz znaleźć tutaj.

Podsumowanie

Aby przećwiczyć lepiej interfejsy mam dla Ciebie przygotowane zadanie, zadanie, które polega na ciągłym rozwijaniu aplikacji. Już mówię co masz zrobić.

Na początku napisz interfejs:

ProductService – będzie on zawierał metody odpowiedzialne za:

  • zwrócenie wszystkich Products
  • zwrócenie ilości produktow na liście
  • zwrócenie Product poprzez productName – jeżeli nie istnieje to null
  • sprawdzenie czy ilość produktu jest większa od 0 dla konkretnego productName
  • sprawdzenie czy produkt o podanym productName istnieje
  • sprawdzenie czy produkt o podanym id istnieje

Następnie napisz do każdej metody co najmniej dwa testy – przypadek pozytywny czyli Assert.assertEquals, oraz przypadek negatywny czyli Assert.assertNotEquals.

Następnie napisz klasę ProductServiceImpl, która będzie implementowała interfejs ProductService. Co za tym idzie wszystkie metody tam zdefiniowane, wszystkie operację wykonaj na liście List<Product> – tak samo jak zrobiliśmy to w przypadku UserServiceImpl.

Moje rozwiązanie jak zawsze możesz podejrzeć tutaj. 😉

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
6 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Poul
Poul
5 lat temu

Taka mała dygresja, że chyba prościej byłoby w metodzie removeUserById() użyć pętli foreach, czyli
for(User user : users)
{
if(user.getId() == userId)
{
users.remove(user);
break;
}
}

bardzo ważna jest instrukcja break na końcu, bez tego mamy exception, chociaż używając klasy CopyOnWriteArrayList zamiast ArrayList, to break nie będzie potrzebny, można też użyć Iteratora zamiast pętli, jest sporo opcji.

Kamil Klimek
Kamil Klimek
5 lat temu
Reply to  Poul

Podczas usuwania elementu z listy nie możesz użyć pętli foreach, ponieważ otrzymasz wyjątek: UnsupportedOperationException. Niestety do usuwania elementu z listy musisz użyć indeksów lub iteratora – choć o nim nic nie mówiliśmy w kursie.

Kuba Kostrzewski
Kuba Kostrzewski
5 lat temu

Mam pytanie:
Dlaczego w testach jak wywołujemy sobie jakieś metody z klasy testowanej, które zwracają wartość to przypisujemy ją do stałej?
Np. w klasie ProductServiceTest w linijce 50 mamy:

final int result = productService.getCountProducts();

Czy pominięcie słówka “final” jest błędem?

Kamil Klimek
Kamil Klimek
5 lat temu

Nie trzeba, ale również nie zaszkodzi. Jeżeli test będzie dłuższy to mamy pewność, że gdzieś przypadkiem jej nie zmienimy. 😉

Łukasz
Łukasz
5 lat temu

Witam,

mam pytanie odnoście gita, mianowicie, w jaki sposób dodać jeden konkretny plik?
Po wpisaniu np:

git add UserService

wychodzi błąd:

fatal: pathspec ‘UserService’ did not match any files

Tzn. że należy wpisać ścieżkę do folderu?

Kamil Klimek
Kamil Klimek
5 lat temu
Reply to  Łukasz

Tak musisz wpisać ścieżkę pliku zaczynając od roota projektu czyli np. srcmainjavaserviceUserService.java

6
0
Would love your thoughts, please comment.x