Java and Kotlin: A Practical comparison (part II)

josealonso

Jos茅 Ram贸n (JR)

Posted on January 4, 2024

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.

馃挅 馃挭 馃檯 馃毄
josealonso
Jos茅 Ram贸n (JR)

Posted on January 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related