DEV Community

José Ramón (JR)
José Ramón (JR)

Posted on

Java and Kotlin: A Practical comparison (part II)

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();
}
Enter fullscreen mode Exit fullscreen mode
Optional.ofNullable(null)
        .ifPresentOrElse(
                email -> System.out.println("Sending email to " + email),
                ()    -> System.out.println("Cannot send email"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2.- Overloading Methods

void log(String msg) { ......... };
void log(String msg, String level) { ......... };
void log(String msg, String level, String ctx) { ......... };
Enter fullscreen mode Exit fullscreen mode

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"
) { 
......... 
}
Enter fullscreen mode Exit fullscreen mode
log(level="DEBUG", msg="trace B")
Enter fullscreen mode Exit fullscreen mode

3.- Utility static methods

final class NumberUtils {
  public static boolean isEven(final int i) {
    return i % 2 == 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

4.- Factory

public class NotificationFactory {

  public static Notification createNotification(
        final NotificationType type
  ) {
      return switch(type) {
        case Email -> new EmailNotification();
        case SMS -> new SmsNotification();
      };
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

KOTLIN

This pattern is built into the Kotlin language. It's lazy and thread-safe.

object Singleton {
  val myProperty......
  fun myInstanceMethod() {
    ...............
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

KOTLIN

val list = listOf("A", "B", "C")
for (elem in list) {
  println(elem)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

8.- Strategy pattern

Implementation with interfaces

This is the classical approach, shown in Kotlin.

fun interface PaymentStrategy {
    fun charge(amount: BigDecimal) : PaymentState
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

This is the resulting class:

class ShoppingCart2(private val paymentStrategy: PaymentStrategy) {
    fun process(totalPrice: BigDecimal) = paymentStrategy.charge(totalPrice)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

This is how it's used:

val creditCardPaymentProcessor = { amount: BigDecimal -> ... }

val payPalPaymentProcessor = { amount: BigDecimal -> ... }
Enter fullscreen mode Exit fullscreen mode

**JAVA

In Java, function types have a strange syntax.

interface PaymentProcessor {
    public Function<BigDecimal, PaymentState> process;
};
Enter fullscreen mode Exit fullscreen mode

This is how it's used:

class creditCardPaymentProcessor implements PaymentProcessor {
    @Override
    public Function<BigDecimal, PaymentState> process = .....;
};
Enter fullscreen mode Exit fullscreen mode

It's quite annoying having to create a class per strategy.

Top comments (1)

Collapse
 
siy profile image
Sergiy Yevtushenko
  • Optional is not a pattern, it's an implementation of monad pattern.
  • Optional can (and should) be used for parameters and fields.
  • 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:

public sealed interface Notification {
    enum NotificationType {
        SMS,
        EMAIL
    }

    record SmsNotification implements Notification () {}
    record EmainNotification implements Notification() {}

    static Notification notification(NotificationType type) {
        return switch (type) {
            case SMS -> new SmsNotification();
            case EMAIL -> new EmainNotification();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public PaymentState creditCardPaymentProcessor(BigDecimal amount) {
...
   return PaymentState.PAID;
}

...
 var paymentState = shoppingCard.process(this::creditCardPaymentProcessor);
Enter fullscreen mode Exit fullscreen mode

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.