Madde 34’de enumlardan bahsederken enum türlerinin kalıtılamayan (final
) sınıflara dönüştürüldüğü söylemiştik. Dolayısıyla bir enum türü başka bir enum türünü kalıtamaz. Enum türleri zaten dolaylı olarak java.lang.Enum
sınıfından türetildikleri için başka sınıfları kalıtması da mümkün değildir.
Bu durum çoğu zaman bir sorun teşkil etmese de bazı enumları kalıtma ihtiyacı duyabiliriz veya kullanıcılara bu imkanı vermek isteyebiliriz. Buna örnek olarak işlem kodlarını (operation code) temsil eden enumları verebiliriz. İşlem kodları bir makina üzerinde yapılabilecek işlemleri temsil ederler, tıpkı Madde 34’de yazdığımız dört işlem yapabilen bir hesap makinasını tanımlayan Operation
gibi. Bu tür enumlar için kullanıcılara esneklik sağlayarak yeni işlem kodları eklemelerine izin vermek isteyebiliriz.
Enumlar kalıtımı desteklemese de arayüzleri kullanarak böyle bir etki yaratabiliriz. Aşağıdaki gibi tek metotlu bir arayüz tanımlayıp enum türünde bu arayüzü uygulayabiliriz. Şimdi BasicOperation
enum türünü bu şekilde yazalım:
// Arayüz uygulayarak kalıtılabilirliği taklit eden enum
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y; }
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
Eğer bir kullanıcı BasicOperation
türünde tanımlı aritmetik işlemlere eklemeler yapmak isterse bu enumu kalıtamaz ancak Operation
arayüzünü uygulayan ikinci bir enum yazabilir. Örneğin, bu programa üst alma ve bölme işleminde kalan hesaplama işlemlerini de eklemek isteyelim. Bunun için yapmamız gereken bu iki işlemi gerçekleştiren yeni bir enum türünü Operation
arayüzünü uygulayarak yazmak olacaktır:
// Kalıtımı taklit eden enum
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
Böylece tür olarak Operation
bekleyen metotlara hem BasicOperation
nesnelerini hem de yeni yazdığımız ExtendedOperation
nesnelerini geçebiliriz. Hatta sadece nesneleri değil ExtendedOperation
türünün kendisini de BasicOperation
yerine metotlara geçip elemanlarını kullanabiliriz. Aşağıdaki program komut satırından verilen iki argümanı kullanarak ExtendedOperation
enumunda tanımlı bütün aritmetik işlemleri gerçekleştiriyor ve sonuçları ekrana yazdırıyor:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(
Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
Burada main
metodunun test
metodunu çağırırken ExtendedOperation.class
geçmesi sayesinde EXP
ve REMAINDER
işlemleri gerçekleşmektedir. Bu kullanım sınırlandırılmış tür belirtecine bir örnektir. (Madde 33) Metottaki opEnumType
parametresinin <T extends Enum<T> & Operation> Class<T>
biçimindeki tür tanımı da geçilebilecek Class
nesnesinin hem bir enum olması hem de Operation
arayüzünü uygulaması gerektiğini belirliyor. Bize lazım olan da tam olarak budur.
Burada ikinci bir seçenek olarak metoda Class
nesnesi yerine Collection<? extends Operation>
geçebiliriz. Bu kullanım sınırlandırılmış joker tür olarak bilinir. (Madde 31)
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet) {
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
Bu kod biraz daha anlaşılır durumdadır ve test
metodu biraz daha esnektir çünkü metodun istemcisi Operation
türünü uygulayan farklı enum türlerinden nesneleri birleştirip gönderebilir. Diğer taraftan ise enum nesneleri aynı türden olmadığı için EnumSet
(Madde 36) ve EnumMap
(Madde 37) ile kullanılması mümkün olmaz. Buradaki iki yaklaşım da API tanımlarken gerçek enum türlerini değil bunların ortak olarak uyguladığı arayüz türünü kullandığı için bize bu esnekliği sağlayabilmektedir.
Bu iki program da komut satırından 2
ve 4
değerleri geçildiğinde aşağıdaki sonucu üretecektir:
4.000000 ^ 2.000000 = 16.000000 4.000000 % 2.000000 = 0.000000
Arayüz kullanarak kalıtılabilirliği taklit etme yönteminin bir dezavantajı, bir enumda tanımlı metotların diğer metot tarafından kullanılamamasıdır. Bu metodun davranışı nesnenin durumuna bağımlı değilse arayüz içinde varsayılan metot (default method) olarak yazılabilir. (Madde 20) Bizim örneğimizde BasicOperation
ve ExtendedOperation
enumları symbol
alanının saklanması ve okunması amacıyla yazılmış bölümleri tekrar etmektedirler. Bu örnekte tekrar edilen kod miktarı çok az olduğu için farketmez ama artarsa ortak kısımları bir yardımcı sınıf veya statik bir yardımcı metot içine taşıyabilirsiniz.
Bu maddede anlatılan teknik Java kütüphanelerinde de kullanılmaktadır. Örneğin java.nio.file.LinkOption
enum türü CopyOption
ve OpenOption
arayüzlerini uygulamaktadır.
Özetle, kalıtılabilen enum türleri yazamasak da bu etkiyi verebilmek için bir arayüz tanımlayıp yazdığımız enumda bunu uygulayabiliriz. Bu istemcilere kendi enum türlerini (veya sınıflarını) aynı arayüzü uygulayarak yazmalarına izin verir. Bu farklı türlerin nesneleri de referans olarak arayüz türünü kabul eden her yerde kullanılabilir.