Design Patterns
We will analyze how some design patterns are implemented in both languages.
1.- Optional Pattern
In Java, Optional doesn't solve the Null Pointer Exception or NPE problem. It just wraps it and "protects" our return values.
Optional<String> getCity(String user) {
var city = getOptionalCity(user);
if (city != null)
return Optional.of(city);
else
return Optional.empty();
}
Optional.ofNullable(null)
.ifPresentOrElse(
email -> System.out.println("Sending email to " + email),
() -> System.out.println("Cannot send email"));
Optional is useful for returning types, but it should not be used on parameters or properties.
getPermissions(user, null);
getPermissions(user, Optional.empty()); // Not recommended
KOTLIN
Solution: Nullability is built into the type system. Kotlin embraces null.
String? and String are different types. T is a subtype of T?.
val myString: String = "hello"
val nullableString: String? = null // correct!!
In Kotlin, all regular types are non-nullable by default unless you explicitly mark them as nullable. If you don't expect a function argument to be null, declare the function as follows:
fun stringLength(a: String) = a.length
The parameter a has the String type, which in Kotlin means it must always contain a String instance and it cannot contain null.
An attempt to pass a null value to the stringLength(a: String) function will result in a compile-time error.
This works for parameters, return types, properties and generics.
val list: List<String>
list.add(null) // Compiler error
2.- Overloading Methods
void log(String msg) { ......... };
void log(String msg, String level) { ......... };
void log(String msg, String level, String ctx) { ......... };
KOTLIN
In kotlin we declare only one function, because we have default arguments and named arguments.
fun log(
msg: String,
level: String = "INFO",
ctx: String = "main"
) {
.........
}
log(level="DEBUG", msg="trace B")
3.- Utility static methods
final class NumberUtils {
public static boolean isEven(final int i) {
return i % 2 == 0;
}
}
In some projects we may end up declaring the same utility function more than once.
KOTLIN
fun Int.isEven() = this % 2 == 0 // Extension function
2.isEven()
4.- Factory
public class NotificationFactory {
public static Notification createNotification(
final NotificationType type
) {
return switch(type) {
case Email -> new EmailNotification();
case SMS -> new SmsNotification();
};
}
}
KOTLIN
In Kotlin a function is used instead of an interface.
// This would be a code smell in Java
fun Notification(type: NotificationType) = when(type) {
NotificationType.Email -> EmailNotification()
NotificationType.SMS -> SmsNotification()
}
}
val notification = Notification(NotificationType.Email)
5.- Singleton
// Much code, it's not even thread-safe
public final class MySingleton {
private static final MySingleton INSTANCE;
private MySingleton() {}
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
}
KOTLIN
This pattern is built into the Kotlin language. It's lazy and thread-safe.
object Singleton {
val myProperty......
fun myInstanceMethod() {
...............
}
}
6.- Iterator
This can be applied only to collections, not to user defined classes.
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
var iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element); // A, B, C
}
KOTLIN
val list = listOf("A", "B", "C")
for (elem in list) {
println(elem)
}
This can be applied to any class that has the iterator operator function defined.
class School(
val students: List<Student> = listOf(),
val teachers: List<Teacher> = listOf()
)
operator fun School.iterator() = iterator<Person> { // Extension function
yieldAll(teachers)
yieldAll(students)
}
val mySchool = School()
for (person in mySchool) {
println(person)
}
Likewise, the operator function compareTo must be used to compare objects.
7.- Comparable
class School(val students: List<Student>, val teachers: List<Teacher>)
override fun School.compareTo(other: School) =
students.size.compareTo(other.students.size)
fun main() {
val school1 = School(listOf(Student("John"), Student("Alice")), listOf(Teacher("Mr. Smith")))
val school2 = School(listOf(Student("Bob"), Student("Eve"), Student("Carol")), listOf(Teacher("Mrs. Johnson")))
if (school1 > school2) {
println("$school1 has more students than $school1")
}
}
8.- Strategy pattern
Implementation with interfaces
This is the classical approach, shown in Kotlin.
fun interface PaymentStrategy {
fun charge(amount: BigDecimal) : PaymentState
}
Next, we implement the interface for all the different payment methods we want to support:
class CreditCardPaymentStrategy : PaymentStrategy {
override fun charge(amount: BigDecimal) : PaymentState = PaymentState.PAID
}
class PayPalPaymentStrategy : PaymentStrategy {
override fun charge(amount: BigDecimal) = PaymentState.PAID
}
This is the resulting class:
class ShoppingCart2(private val paymentStrategy: PaymentStrategy) {
fun process(totalPrice: BigDecimal) = paymentStrategy.charge(totalPrice)
}
Implementation with Function Types
This implementation is easier to read than the previous one, but it's less reusable and less maintainable.
class ShoppingCart(private val paymentProcessor: (BigDecimal) -> PaymentState) {
fun process(totalPrice: BigDecimal) = paymentProcessor(totalPrice)
}
typealias PaymentStrategy = (BigDecimal) -> PaymentState
class ShoppingCart(private val paymentProcessor: PaymentStrategy) {
fun process(totalPrice: BigDecimal) = paymentProcessor(totalPrice)
}
This is how it's used:
val creditCardPaymentProcessor = { amount: BigDecimal -> ... }
val payPalPaymentProcessor = { amount: BigDecimal -> ... }
**JAVA
In Java, function types have a strange syntax.
interface PaymentProcessor {
public Function<BigDecimal, PaymentState> process;
};
This is how it's used:
class creditCardPaymentProcessor implements PaymentProcessor {
@Override
public Function<BigDecimal, PaymentState> process = .....;
};
It's quite annoying having to create a class per strategy.
Top comments (7)
Kotlin "double" type system is not 100% null safe and, actually, suffers from several negative consequences. It encourages use of optional chaining, which, in turn, often is used to look deep into object structure, causing deep coupling of the code. Optional chaining is not composable, unlike monad transformations.
Extension methods actually suffer from the duplication much more than utility methods. Utility methods are grouped inside dedicated classes, but extension methods usually scattered across the whole code base and often remain invisible, and this encourages repeating them. Usually, there are rules for placement of extension methods to prevent uncontrolled scattering. Lack of tooling makes following these rules an additional task for code reviews, harming productivity.
I see no point in creating a dedicated class/interface for factory. Making it a method in the Notification makes much more sense. Full implementation would look like so:
Full code in Kotlin will be barely more concise.
Correct implementation of singleton in Java usually uses enum and is lazily loaded and thread safe.
Iterator is an interface and nothing prevents you from implementing it for your classes too.
Why not implement Comparable interface for your class directly, instead of using extension method? Actually, extension methods available in Java too with Manifold compiler plugin.
Functions have strange, inconsistent syntax in Kotlin. In Java, they follow general language design. And you don't have to implement class per strategy, nothing prevents you from passing lambda or, even better, method reference:
Overall, I think the comparison of Kotlin with Java makes not so much sense. In fact, it would be better for Kotlin if its proponents stop comparing it to Java, as Kotlin gradually loses points as Java evolves. The use of modern Java language features, combined with functional style and some non-traditional techniques, makes Java quite concise and expressive. Inherently more clean syntax, lacking zero-value noise of "fun" and ":", makes Java also more readable. You can take a look at this example, to get a glimpse of what modern Java can look like.
Many thanks for taking the time to write that long comment.
I now agree that modern Java is not as verbose as it was. But many legacy codebases can't upgrade its Java version.
That's not true. Java is exceptionally backward compatible. And even without changing a single line of code, just by switching to a new version, it is possible to get significant performance improvement.
@siy, thanks for your quick response. Next time I have a legacy codebase in Java 8 at work, I will take that possibility into consideration. Although it's probably not straight, because a new way of organizing the packages was introduced in Java 9.
I do know that the garbage collector keeps improving and changing, I will look into it.
As one who migrated several projects through 8 -> 11/17 -> 21 Java versions, I know that most painful part of migration are Gradle build scripts (yet another reason to avoid Gradle). Maven-based projects had no such issues. Otherwise all is necessary is to bump versions of dependencies/plugins (and configure Java version, of course).
Excellent info. I didn't know that about Gradle. Most of the Java projects are in Maven, but for a new project I thought Gradle is better, because you get compile errors if you don't use the right syntax, assuming you write Gradle in Kotlin, of course.
Unlike a fully declarative Maven configuration (which is checked by any modern IDE by the way, POM has an XML schema and IDE automatically validates it), Gradle uses a mix of declarative and imperative approaches. This results in the uniqueness of each configuration, unlike Maven, where everything is the same, no matter who wrote the original configuration and who made updates.
Things are complicated by poor documentation and cryptic error messages, which often point in the wrong direction. Sometimes clues (not solutions!) could be found after intensive googling in some obscure forum chats, where discussed Gradle issues which only tangentially related to one you have.
Unlike Maven, the supported Java version directly linked to the Gradle version, so you have no choice and must update the Gradle version.
So, if I have at least minimal influence on the decision, I'm always strongly against Gradle.