10 lat temu w Effective java mogliśmy przeczytać by nigdy nie używać finalize. Ja się zastosowałem i tylko raz niechcący w ferworze refaktoringu przezwałem jakąś metode na finalize i zaczęły dziać się śmieszne rzeczy. Latka minęły i świeżo na półeczki księgarni informatyka wyszło trzecie wydanie Effective java z apdejtem do Java 9. No i w tej najnowszej Javie (najnowszej jeszcze tylko 2 miesiące!) w rozdziale o finalize pojawia się nowy mechanizm java.lang.ref.Cleaner
Koniec końców Joshua Bloch napisał, że ten Cleaner tez zjebany ale ogólniej mniej i jest taki jeden kejs gdzie go bez strachu użyć można. No i tak się składa, że moim zdaniem ten przykład użycia jednocześnie niesie ze sobą ogromną wartość edukacyjną jak właściwie enkapsulować stan w klasie. A do tego możemy sobie zobaczyć jak inne języki na JVM poradzą sobie ze standardową sytuacją w Javie.
Przez chwile miałem wątpliwości czy aby temat będzie dla ogółu interesujący ale potem sobie przypomniałem, że to mój blog i będę tutaj pisał co mi się chce.
Dobra Enkapsulacja
Na poczatku wytniemy kawałeczki kodu by łatwiej było zauważyć wspomnianą enkapsulację i ogólnie pojęte ukrywanie informacji, które bardzo pomaga w utrzymaniu i rozbudowie systemu. Jeśli ktoś chce brzmieć bardziej Fancy to może używać terminu anglojęzycznego https://en.wikipedia.org/wiki/Information_hiding. Jeśli ktoś chce brzmieć bardziej biznesowo to może powiedzieć, że informejszyn hajding redukuje ołweral meintenance cost and reduces tajm to market for fiuczer rilises.
public class InstanceAroundResource implements AutoCloseable{ //We will delegeta cleaning to Cleaner from Java 9 private static final Cleaner cleaner=Cleaner.create(); //This is definition of internal state, static -> so it has no ref to external instance //private - to better hide information private static class EncapsulatedResource implements Runnable{ (...) } //no getters for those two fields, no strange hakiers annotations either private final EncapsulatedResource state; //this triggers cleaning procedure private final Cleaner.Cleanable cleanable; public InstanceAroundResource(String resourceId) { //notice that both instances are created inside constructor , no direct assignment, //no information how resourceId is escapes outside //compare this with stntaxt 'this.field = field' which is hiding information like in the sentence // "think about number seven but don't tell what the number is" this.state = new EncapsulatedResource("[opened :"+resourceId+"]"); this.cleanable = cleaner.register(this, state); } // here we are connecting Cleaner with Autocloseable from Java7 @Override public void close() throws Exception { cleanable.clean(); } }
I teraz tak, po pierwsze primo od javy 9 mamy import do dyspozycji:
import java.lang.ref.Cleaner;
i idąc dalej :
- Zauważcie, że nic z tej klasy nie ucieka poza abstrakcyjnem resourceId przekazanym do konstruktora - to jest niejako kawałek publicznego interfejsu.
- Drugim kawałkiem publicznego interfejsu jest - no własnie interfejs - Autocloseable , czyli obietnicę posiadania metody/zachowania close
- Totalnie ukryliśmy przed użytkownikiem użycie Cleanera. Nie ma ani dziwnych ch** wie po co getterów setterów.
- Jak Bloch przykazał pola są finalne - przynajmniej pod katem referencji wnętrze jest niezmienne.
- Ukryliśmy przed światem zewnętrznym istnienie klasy EncapsulatedResource co daje nam pełne pole do refaktoringu tego kawałka kodu w przyszłości (i minimalizacji tajm to market)
- Jeszcze raz podkreslmy, że parametry konstruktora również nie zdradzają za wiele o bebechach.
- Sam Cleaner z tego co zrozumiałem jest lepszy od starej metody finalize pod tym katem, że delegując czyszczenie do Cleanera - który nasłuchuje (listenerek taki) - nie będzie problemu z wyjątkami przy sprzątaniu bo Cleaner ma pełną kontrolę nad tym watkiem/procedurą i ogarnia. Tak przynajmniej napisał autor i ja mu wierzę!
I cały kod
import java.lang.ref.Cleaner; public class InstanceAroundResource implements AutoCloseable{ private static final Cleaner cleaner=Cleaner.create(); //static to not have reference to external instance private static class EncapsulatedResource implements Runnable{ String handleToSystemResource; //don't need to be private because EncapsulatedResource is private EncapsulatedResource(String handleTosystemResource) { this.handleToSystemResource = handleTosystemResource; } @Override public void run() { System.out.println("Closing system resource by cleaner :"+handleToSystemResource); handleToSystemResource="CLOSED"; } } private final EncapsulatedResource state; private final Cleaner.Cleanable cleanable; public InstanceAroundResource(String resourceId) { this.state = new EncapsulatedResource("[opened :"+resourceId+"]"); this.cleanable = cleaner.register(this, state); } @Override public void close() throws Exception { System.out.println("First In Auto-Closable"); cleanable.clean(); } }
Zerknijmy teraz na odpalenie tego :
//1 try(InstanceAroundResource r = new InstanceAroundResource("BLOG-POST")){ System.out.println("Using resource1"); } //2 new InstanceAroundResource("UNHANDLED-RESOURCE"); System.out.println("r2 left alone"); //System.gc();
sytuacja nr 1 prosta bo na końcu zostanie wykonane close i mamy
Using resource1 First In Auto-Closable Closing system resource by cleaner :[opened :BLOG-POST]Co do drugiej sytuacji to podobnie jak u autora książki także i u mnie backup nie zadziałał bez sztucznego wywołania System.gc()
r2 left alone Closing system resource by cleaner :[opened :UNHANDLED-RESOURCE]
A Java 10 ?
W Javie 10 zadziała już poniższe :
jshell> try(var r=new InstanceAroundResource("Java10")){ ...> System.out.println("Doing something in java 10"); ...> }
Doing something in java 10 First In Auto-Closable Closing system resource by cleaner :[opened :Java10]
Czyli już nie trzeba deklarować nowego InstanceAroundResource z zaznaczeniem iż jest typu InstanceAroundResource. Mniej czytania, inżynier może przeznaczyć moc operacyjną muzgu na redukcję tajm to market.
Inne Języki
Kotlin
Kotlin ma coś takiego :
public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R { var exception: Throwable? = null try { return block(this) } catch (e: Throwable) { exception = e throw e } finally { this.closeFinally(exception) } }
Co oznacza, że mechanizmem "extends method" dorzuci metodę use do wszystkiego co rozszerza AutoCloseable. Także zadziała to i z naszym - napisanym w Javie - InstanceAroundResource.
val r=InstanceAroundResource("KOTLIN-EXAMPLE") r.use { r: InstanceAroundResource -> println("Doing something in kotlin") }I to w sumie w niektórych kręgach półświatka programistycznego można nazwać metodą/funkcją wyższego rzędu. Wynik będzie analogiczny z przykładem Javowym :
Using resource1 First In Auto-Closable Closing system resource by cleaner :[opened :BLOG-POST]
No i analogie idzie dalej, jeśli chcemy by cleaner zadziałał jako backup trzeba ręcznie gc wywołać :
InstanceAroundResource("UNHANDLED-IN-KOTLIN") println("r2 left alone") System.gc()
A co z nullem
Ogarnianie nulli z javy to jeden z lepszych fiuczerów Kotlina, zerknijmy czy jebnie coś takiego :
val r3:InstanceAroundResource? = null r3.use { r: InstanceAroundResource? -> println("jebnie?") } //nie jebnieNo i po pierwsze nie da się łatwo oszukać kompilatora, że null jest typu InstanceAroundResource (ten taki znak zapytania na końcu) a po drugie to nie jebnie. Jak zerkniesz jeszcze raz na implementacje "use" to zobaczysz , ze w finally nie ma "close" a jest " this.closeFinally(exception)". I to wygląda mniej więcej tak :
internal fun AutoCloseable?.closeFinally(cause: Throwable?) = when { this == null -> {} cause == null -> close() else -> try { close() } catch (closeException: Throwable) { cause.addSuppressed(closeException) } }
Czyli takie zamknięcie-otwarcie RISORSA nie jest rzeczą prostą. I całe szczęście, że Java i Kotlin mają gotowce na to w standardowej bibliotece. I tutaj też zwróć uwagę, że Java natywnie na poziomie języka a kotlin zaimplementowane przy pomocy bardziej abstrakcyjnego natywnego mechanizmu - "rozszerzalnych metod". Zaraz zobaczymy, ze to może iść jeszcze dalej.
Scala
I teraz tak : wiem, że google jakoś segreguje wyniki wyszukiwania na podstawie ciasteczek i mogą być dla każdego unikalne ale jak wpisałem w googla własnie "scala try with resources " to kurcze same tutoriale wyskakują jak samemu napisać a nie byłem w stanie jakiejś natywnej konstrukcji odnaleźć.
jak ktoś zna to niech da namiar bo inaczej ryzykujemy w każdym projeckie "not inwented here" z pytaniem "a w której paczce mamy zaimplementowany loan-pattern, no wiesz ten try z javy?". Jest biblioteczka https://github.com/jsuereth/scala-arm ale tez jakos nie jest dobrze na googlu spozycjonowana a znalazłem ją bo już o niej kiedyś słyszałem. W każdym razie jak już biblioteczkę mamy to dalej kod wygląda bardzo podobnie:
val r=new InstanceAroundResource("SCALA-EXAMPLE") val mr: ManagedResource[InstanceAroundResource] = managed(r) mr.foreach{r=> println("Doing something in scala") } new InstanceAroundResource("UNHANDLED-IN-SCALA") println("r2 left alone") System.gc()
Doing something in scala First In Auto-Closable Closing system resource by cleaner :[opened :SCALA-EXAMPLE] r2 left alone Closing system resource by cleaner :[opened :UNHANDLED-IN-SCALA]
Poziomy abstrakcji mechanizmów językowych
Obczajcie to :
- Java ma try-with-resources
- Kotlin ma extension method przy pomocy, którego można zaimplementować try-with-resources i inne rozszerzenia
- Scala ma implicity przy pomocy których można zaimplementować extension method i inne meta-rozszerzenia przy pomocy których można zaimplementować try-with-resources i inne rozszerzenia
Brak komentarzy:
Prześlij komentarz