Programowanie średnio-zaawansowane #27: klasy wewnętrzne

Ostatnim tematem na poziomie średnio-zaawansowanym, o którym opowiem, są klasy wewnątrz klasy. Wiem, że brzmi to dość enigmatycznie, ponieważ, dotąd pokazywałem jedynie kod zawarty tylko w jednej klasie. Jednak język Java daje dużo większe możliwości, niż na pierwszy rzut oka, można by się spodziewać.

Opiszę tutaj cztery rodzaje klas w innej klasie:

  • inna klasa w tym samym pliku
  • klasa wewnętrzna
  • klasa statyczna
  • klasa anonimowa

No to zabieram się do pracy. Na początek napiszę sobie przykładową klasę o nazwie Container.

public class Container {
	private final int id = 1;
	private String name;
	
	public Container() {
		this.name = "default";
	}
	
	public void write () {
		System.out.println("Id is: " + id + ", name: " + name);
	}	
}

Nic specjalnego na razie tutaj się nie dzieje. Metoda write zwróci name o wartości default.

Nie ma żadnych przeszkód, aby w tym samym pliku trzymać więcej niż jedną klasę. Reguła jednak jest taka, że przynajmniej jedna z nich powinna mieć taką samą nazwę jak nazwa pliku oraz używać publicznego modyfikatora dostępu.

public class Container {
	private final int id = 1;
	private String name = "default";
	
	public Container() {
		this.name = new AnotherContainer("test").getName();
	}
	
	public void write () {
		System.out.println("Id is: " + id + ", name: " + name);
	}	
}

class AnotherContainer {
        private final int id = 2;
	private final String name;
	
	public AnotherContainer(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}

        public void write () {
		System.out.println("Id is: " + id + ", name: " + name);
	}
}

Spróbuj stworzyć trzecią klasę z main i odpalić obie klasy. Następnie przenieś nowo utworzoną klasę do innego pakietu. Teraz ten sam kod nie powinien się skompilować, ponieważ klasa AnotherContainer jest o dostępie domyślnym i nie możesz jej używać z poza jej pakietu.

Przykładowy wynik napisanej metody main.

Id is: 1, name: test, helper class: container class

Klasa wewnętrzna może mieć dowolny modyfikator dostępu. W kolejnym przykładzie przetestuję jedynie wersję z modyfikatorem private.

Prywatna klasa wewnętrzna:

public class Container {
	private final int id = 1;
	private String name = "default";
	
	public Container() {
		this.name = new AnotherContainer("test").getName();
	}
	
	public void write () {
		System.out.println(new ContainerHelper().write());
	}
	
	private class ContainerHelper {
		String description = "helper class name is";
		
		String write() {
			return description + ": " + ContainerHelper.class.getName() + ", id: " + id + ", name: " + name; 
		}
	}
}

Zaszła to ciekawa wymiana danych. Z jednej strony klasa zewnętrzna ma możliwość używania elementów klasy wewnętrznej, a z drugiej okazuje się, że to samo można zrobić w odwrotną stronę. Spójrz na metodę write z klasy ContainerHelper. Odwołuje się ona do pól z klasy Container. Spytasz się, jak to możliwe? Dzieje się tak dlatego, że są one przekształcane w zwykłe klasy z ukrytą zmienną instancyjną wskazującą na instancję klasy zewnętrznej.

Wszystkie powyższe możliwości programowania klas są mało praktyczne i rzadko używane, a nawet niewskazane. Mimo to są przypadki klas wewnętrznych, które będziesz używać dość często. Są to klasy statyczne. Chociaż nie możesz stworzyć publicznej (głównej) klasy jako statycznej, ale nie ma żadnych przeszkód, aby ją zawrzeć jako klasę prywatną wewnątrz innej publicznej. Przykładem wzorca projektowego Javy*, gdzie często stosuje się klasę statyczną, jest wzorzec budowniczy (ang. builder), który ułatwia tworzenie obiektów, gdy Twoja klasa posiada bardzo dużą liczbę akcesorów.

public class BuilderPattern {
	private final Long id;
	private final String firstname;
	private final String lastname;
	private String street;
	private String code;
	private Integer number;
	private String city;

	public BuilderPattern(Builder builder) {
		this.id = builder.id;
		this.firstname = builder.firstname;
		this.lastname = builder.lastname;
		this.street = builder.street;
		this.code = builder.code;
		this.number = builder.number;
		this.city = builder.city;
	}

	public BuilderPattern() {
		throw new UnsupportedOperationException("Not supported yet.");
	}

 // gety i sety, jesli potrzebujesz

	public static class Builder {
		private Long id;
		private String firstname;
		private String lastname;
		private String street;
		private String code;
		private Integer number;
		private String city;

		public Builder(Long id, String firstname, String lastname) {
			this.id = id;
			this.firstname = firstname;
			this.lastname = lastname;
		}

		public Builder street(String street) {
			this.street = street;
			return this;
		}

		public Builder code(String code) {
			this.code = code;
			return this;
		}

		public Builder number(Integer number) {
			this.number = number;
			return this;
		}

		public Builder city(String city) {
			this.city = city;
			return this;
		}

		public BuilderPattern build() {
			return new BuilderPattern(this);
		}
	}

// toString
}

Stworzyłem klasę BuilderPattern, która posiada bardzo dużą liczbę pól. Kilka z nich to pola finalne. Nie są one wymagane w każdej realizacji wzorca builder, ale jest to przypadek często stosowany, także zastosowałem go tutaj. Teoretycznie wystarczyłoby przypisać polom finalnym jakieś wartości i stworzyć odpowiednie konstruktory, aby mieć opcje stworzenia obiektu z różnymi wartościami. Jednak taka implementacja, powoduje, że kod jest dużo dłuższy. Możesz też po prostu inicjować odpowiednie zmienne za pomocą setterów, choć przyznasz, że nie jest to zbyt wygodne rozwiązanie. A co gdyby udało się stworzyć wybrany obiekt, w zależności, czy jakiegoś atrybutu potrzebujesz czy nie? Tak działa właśnie wzorzec budowniczy.

Wzorzec ten wyróżnia klasa statyczna, która zawiera jeden konstruktor, definiujący zmienne finalne. Każde inne pola posiadają odpowiednik metody typu setter, która jednak nie zwraca nazwę pola, tylko obiekt klasy statycznej, z przypisanym wcześniej wartością do zmiennej. Na końcu zaimplementował metodę build, zwracającą wskaźnik this na końcowy obiekt, który ma być stworzony.

Wygląda to skomplikowanie, ale tak nie jest. Teraz pokażę Ci, jak stworzyć obiekt, takiej klasy. Załóżmy, że oprócz wymaganych argumentów: id, firstname, lastname, chcę stworzyć obiekt zawierający tylko kod pocztowy i miasto.

BuilderPattern myBuilder = new BuilderPattern
				.Builder(1L, "Anna", "Kowalski")
				.code("53-400")
				.city("Wrocław")
				.build();

Wystarczyło napisać new BuilderPattern ale zamiast w nawiasach korzystać z jakiegoś konstruktora, skorzystać właśnie z pomocniczej klasy statycznej. Wpierw musisz wypełnić finalne pola, a następnie dowolnie od Twoich potrzeb możesz inicjować pozostałe wartości. Na koniec, tworzysz instancję całego obiektu, za pomocą operacji build. Czy to nie eleganckie rozwiązanie? Teraz możesz łatwo stworzyć wiele podobnych obiektów, wypełniając opcjonalne pola, w zależności od Twoich potrzeb!

Zawartość obiektu myBuilder:

BuilderPattern [id=1, firstname=Anna, lastname=Kowalski, street=null, code=53-400, number=null, city=Wrocław]

Ostatnią kwestią jest klasa anonimowa. Szerzej tą możliwość, będę opowiadał, w dodatku do kursu dotyczącego Javy 8, ponieważ od tej wersji zaistniała możliwość tworzenia klas anonimowych za pomocą wyrażeń lambda*. W tym przykładzie, pokażę Ci klasyczne użycie tego rodzaju klas.

Klasy anonimowe są wykorzystywane w JDK w kilku miejscach, np. przy tworzeniu wątków lub tzw. listenerów. Pokażę Ci jednak jak możesz sam stworzyć własną klasę anonimową.

Spójrz na przykład poniżej

public abstract class Phone {
	abstract void makeCall();
}

Klasa Phone to zwykła klasa abstrakcyjna z jedną metodą abstrakcyjną. Wcześniej mówiłem Ci, że cechą charakterystyczną tego typu klas jest to, że nie możesz stworzyć obiektu takiej klasy (póki nie będzie ona rozszerzać innej, która już nie jest abstrakcyjna). Jak wszystko w życiu, nie jest to do końca prawda.. Jeśli spróbujesz stworzyć obiekt klasy Phone to IDE, w którym programujesz, prawdopodobnie uzupełni Twój kod podobnie jak poniżej.

Phone phone = new Phone() {
	@Override
	void makeCall() {
	// TODO Auto-generated method stub		
	}
};

Nie mogę w tym przypadku napisać jedynie Phone phone = new Phone(); Natomiast nic nie stoi na przeszkodzie, aby klasę abstrakcyjną uzupełnić o Twoją implementację. Taki zapis nazywa się właśnie klasą anonimową.

public static void main(String[] args) {
	Phone phone = new Phone() {	
		@Override
		void makeCall() {
			System.out.println("Make a call");
		}
	};
	phone.makeCall();
}

Przykładowe użycie klasy anonimowej, nie różni się niczym od używania każdej. To wszystko, jeśli chodzi o bardziej skomplikowane użycie klas w języku Java. Następne lekcje będą dotyczyły już bardziej skomplikowanych programów.

*Wzorce projektowe, to opracowane przez najlepszych programistów Java, ogólne rozwiązania określonych problemów architektonicznych, które umożliwia składnia języka.

**https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

***Klasy nadsłuchujące, szeroko używane do tworzenia aplikacji desktopowych.

Dodaj komentarz