***Note*** This article is not about choosing sides; it’s about crafting a narrative where both languages complement each other.
Java has been like a familiar old friend—reliable and steadfast. Then, along comes Kotlin, a relatively new kid on the block, turning heads and causing a stir. As a seasoned Java developer, you can’t help but feel conflicted. Do you stick to the comforting embrace of Java or venture into the sleek, modern appeal of Kotlin? The ‘better’ tag for new tech sparks loyalty and skepticism. But for experienced developers, this isn’t a hurdle; it’s an opportunity. Instead of a Java-Kotlin battle, it’s a chance to learn from both. I reached out to the community for insights on this ongoing Java-Kotlin debate. This is what I found:
Different developers have varying needs, and in the never-ending debate between Kotlin and Java, one crucial distinction stands out from crowd-sourcing: (non-)nullable types. This is something most Kotlin users believe Java couldn’t effectively replicate. But perhaps they’re mistaken. Depending on whether you’re a Java developer or a Kotlin enthusiast, the answers will differ. Java devs believe Java could adopt Kotlin’s nullability tracking in the type system without breaking backward compatibility. It might resemble the transition from pre-Java 5 to post-Java 5 (when generics were introduced), where all old code had “unknown nullability,” and new code would mark each type as either nullable or not nullable.
I found that the main gripe I heard from developers regarding things like Kotlin, Scala, etc., is that they have numerous intricate and smart approaches to problem-solving instead of emphasizing consistent methods. This can result in developers opting for increasingly complex solutions simply because they seem sophisticated, rather than prioritizing genuine simplicity. Sometimes, verbosity is more straightforward, especially at 4 am when your personal life is chaotic and you’re tackling a production issue affecting 10 million users.
Another argument is that Kotlin, as a language, is good, but its ecosystem doesn’t even belong in the same supercluster as Java’s. Some Java advocates argue that you need to learn Java before delving into Kotlin and that knowing Java is a prerequisite for programming in Kotlin.
Many developers are intrigued by what the next five years hold for Kotlin, especially considering its development by JB and its ties to their business model. However, there’s talk that if JetBrains faces issues, Google might step in, given Kotlin’s crucial role in Android. Kotlin owes much of its current status to Google’s adoption, such as the addition of coroutines, specifically important for Android GUI programming.
Every time someone launches a new language, some fanboys claim it’s better than JAVA.” Yet, despite this, Java remains extensively used!
Well, I feel that everyone agreed on is that the difficult part isn’t necessarily the language. Probably many better-designed programming languages have come and gone, the hardest part is getting adoption. And that every time someone launches a new language, some fanboys claim it’s better than JAVA.
“Once a new technology rolls over you, if you’re not part of the steamroller, you’re part of the road. ― Stewart Brand”
Here are 17 differences:
- Kotlin supports a more functional programming style
- Null Safety
- Observability tools /observable functions
- Syntax
- Checked Exceptions
- Coroutines Support
- Data Classes
- Type Inference
- Extension Functions
- Smart Casts
- Constructors
- Ternary Operator
- Primitive Types
- String Templates
- Operator Overloading
- Wildcard-types vs. Declaration-site Variance
- Multiplatform development
1. Kotlin supports a more functional programming style
Functional programming is a programming paradigm where computation is handled as though you are evaluating mathematical functions. Unlike Object Oriented Programming, functional programming avoids changing state and mutable data. In functional programming, functions are treated as first-class citizens, meaning they can be assigned to variables, passed as arguments to other functions, or returned as values by other functions.
Java is primarily an object programming language, but Java 8 came with some features such as lambda expressions, functional interfaces, streams, the optional class and reference methods that allowed us to write Java in the functional style.
However, Java can’t support some of the functional programming concepts without completely reworking the whole language to prefer functional over regular OOP and invalidate every existing library or provide two radically different ways of approaching problems.
Kotlin doesn’t have this issue; it was designed with the functional programming approach in mind from the ground up.
Kotlin supports higher-order functions, lambda expressions, and extension functions. These features proved a seamless and expressive experience for developers adopting a functional programming paradigm. These features enhance the language’s expressiveness and conciseness, allowing us to write better code.
data class Product(val name: String, val price: Double) fun main() { // Sample list of products val products = listOf( Product("Laptop", 1200.0), Product("Smartphone", 800.0), Product("Headphones", 100.0), Product("Tablet", 500.0), Product("Camera", 700.0) ) // Example 1: Using Map and Reduce to Calculate Total Price val totalPrice = products.map { it.price }.reduce { acc, price -> acc + price } println("Total Price of Products: $totalPrice") // Example 2: Using Filter and Map for Discounted Products val discountedProducts = products.filter { it.price > 500.0 }.map { it.copy(price = it.price * 0.9) } println("Discounted Products: $discountedProducts") // Example 3: Using MaxBy to Find the Most Expensive Product val mostExpensiveProduct = products.maxByOrNull { it.price } println("Most Expensive Product: $mostExpensiveProduct") }
2. Null Safety
Null dereferencing is a common type of programming error in Java. On Android, NullPointerException (NPE) errors have long been the largest cause of app crashes on Google Play.
When Java was incepted, null-safety was not a big concern. Some of the pioneers in the computing world call it a big mistake that has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Various attempts have been made to fix this problem, with initiatives like JSR-305 aiming to address the issue. However, none of these endeavors achieved widespread success. Presently, a range of effective static analysis tools for Java are designed to check nullness. Examples include CheckerFramework, SpotBugs, ErrorProne, and NullAway, providing valuable support in the ongoing battle against null-related issues.
This problem arises because Java allows developers to assign null values to a variable. This can lead to a NullPointerException at runtime if you attempt to access or modify an object reference that points to null. As a developer, you have no option but to handle this exception manually. Here is an example of a Java code that could result in a NullPointerException.
public class Example { public static void main(String[] args) { String name = null; int length = name.length(); // This line will throw a NullPointerException } }
Kotlin addresses this issue through its type-safe system that enforces the concept of null safety through a combination of nullable and non-nullable types. In Koltin, variables are non-null by default. This means that if you want a variable to be nullable, you have to explicitly define it as nullable using the? operator.
val name: String? = null // Nullable String
Kotlin also provides the safe call operator that allows developers to perform a member access operation only if the receiver (the object being accessed) is non-null. Rather than throwing a null exception, the receiver evaluates to null if the receiver is null.
val length: Int? = nullableString?.length
3. Observability tools /observable functions/ observability functionality
Observability is a critical aspect of modern software development, allowing us to gain insights into the runtime behavior of their applications. When comparing the ease of implementing observability features, Kotlin often shines due to its concise syntax and expressive language features.
On the other hand, Java is a more established language with a vast ecosystem of libraries and tools that support observability. These tools include well-established tools like Prometheus, Grafana, and New Relic for observability, logging libraries, such as Log4j and SLF4J, and profiling tools like YourKit and VisualVM.
Both languages support reactive programming paradigms, but Kotlin’s concise syntax and native support for coroutines make it particularly well-suited for writing observable functions in a reactive style.
Kotlin’s interoperability with Java not only simplifies the integration of Java libraries and tools but also opens the door for leveraging cutting-edge development tools designed for optimizing code performance and enhancing developer productivity. One such tool that stands out for its unique approach to observability and performance monitoring is Digma.
Digma is a powerful Integrated Development Environment (IDE) plugin that offers invaluable code-level insights. Unlike traditional observability tools that target DevOps and IT teams, Digma is built as a Continuous Feedback platform for developers that is all about writing performant code.
Both Kotlin and Java developers can also use Digma to understand and debug complex code bases. Digma leverages OpenTelemetry behind the scenes to collect data such as traces, logs, and metrics. Once the data is collected, Digma analyzes it to detect meaningful insights about the code. It looks for regressions, anomalies, code smells, or other patterns that can be useful for knowing about the code and its use, making the debugging process much easier.
4. Syntax
One of the major differences between Java and Kotlin is the conciseness in syntax. Java has long been criticized for being a verbose language. Kotlin requires much less code to achieve the same functionality compared to Java. Therefore, Koltin allows you to write much more readable and maintainable code. Here is an example of how you’d define a collection in Java vs Kotlin.
In Java
List<String> list = new ArrayList<>(); list.add("Java"); list.add("is"); list.add("verbose");
In Kotlin
val list = listOf("Kotlin", "is", "concise")
Kotlin is a statically typed language. A Kotlin compiler can deduce the compile-time types of expressions, lambda expression parameters and properties. This saves developers the trouble of explicitly declaring types.
In Java
List<String> myList = new ArrayList<>();
In Kotlin
val myList = ArrayList<String>()
5. Kotlin does not have checked exceptions
Checked exceptions in Java are checked at compile time, forcing you to either catch the exception or declare it in the method’s throws clause. These exceptions are often beyond the control of your program and may involve I/O errors, network issues, or files not found. Here is an example in Java.
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class FileHandler { // Example of a method with a checked exception public static void readFromFile(String filename) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(filename))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } } // Usage public static void main(String[] args) { try { readFromFile("example.txt"); } catch (IOException e) { e.printStackTrace(); } } }
Checked exceptions have been the subject of debate in Java, leading to their removal in Kotlin. All exceptions in Kotlin are unchecked. This eliminates forced handling and, thus less boilerplate code and also encourages Kotlin developers to write concise code while still being able to handle exceptions effectively. Here is how we can re-write the above logic in Kotlin.
import java.io.BufferedReader import java.io.FileReader import java.io.IOException // Example of a function in Kotlin with no checked exceptions fun readFromFile(filename: String) { try { BufferedReader(FileReader(filename)).use { reader -> var line = reader.readLine() while (line != null) { println(line) line = reader.readLine() } } } catch (e: IOException) { e.printStackTrace() } } // Example usage fun main() { readFromFile("example.txt") }
6. Coroutines Support
Asynchronous programming is key to developing scalable and high-performance applications. Handling tasks asynchronously also allows developers to build applications that are resource-efficient and provide a seamless experience when interacting with their applications.
Java has traditionally relied on promises, callbacks, futures, and other reactive programming libraries to handle asynchronous operations. However, through Project Loom, Java has been aiming to address limitations in the traditional Java concurrency model. In particular, it offers a lighter alternative to threads, along with new language constructs for managing them.
And finally, in the recent JDK 21 release, Virtual threads were finally released as a platform feature. I’m optimistic that this will dramatically reduce the efforts for writing, maintaining, and observing high-throughput concurrent Java applications.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits
Kotlin provides a more structured and readable approach to asynchronous programming through coroutines. Kotlin coroutines also support structured concurrency. Coroutines provide a concise and straightforward way to handle asynchronous programming in Kotlin. They allow us to write asynchronous code that looks and behaves like sequential code, making it more readable and maintainable. This is important for managing tasks such as network requests, database queries, and other I/O-bound operations.
import kotlinx.coroutines.* fun main() { // Launching a new coroutine in the background GlobalScope.launch { println("Coroutine started") // Simulating some asynchronous work val result = withContext(Dispatchers.Default) { // Some background processing delay(1000) 42 } // Print the result after the work is done println("Result: $result") } // Main thread continues its work println("Main thread continues") // Adding a delay to wait for the coroutine to complete (not recommended in production) runBlocking { delay(2000) }
7. Data Classes
In Java, creating a class for holding data typically involves defining variables, constructors, writing getters and setters, and implementing methods like equals(), hashCode(), and toString(). This results in a lot of boilerplate code. Here is an example of a conventional Java bean with private fields, getters, setters, and methods for hashing, equality, and string representation.
import java.util.Objects; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
In Kotlin, we can define the same data class with significantly less boilerplate code. Kotlin provides the data keyword that automatically generates the appropriate member functions for all properties declared inside the primary constructor. Here is how you can define the above data class in Kotlin.
data class Person(val name: String, val age: Int)
8. Type inference differences
Type inference is the ability of a programming language to automatically deduce either partially or fully the type of a variable at compile time—both Kotlin and Java support type inference. However, there are key differences in how they go about it.
In Java, type inference is not as robust as in Kotlin. Java uses the diamond operator <> to simplify the instantiation of generic types, but it primarily applies to generics.
import java.util.ArrayList; import java.util.List; public class JavaTypeInference { public static void main(String[] args) { // Before Java 7 List<String> list1 = new ArrayList<String>(); // With diamond operator (Java 7 and later) List<String> list2 = new ArrayList<>(); } }
On the other hand Kotlin more robust type inference capabilities. Unlike Java, Kotlin can infer types not only for generics but also for variables and return types of functions.
fun main() { // Variable type inference val message = "Hello, Kotlin!" // Compiler infers that message is of type String // Function return type inference fun add(a: Int, b: Int) = a + b // Compiler infers that the return type is Int // Collection type inference val numbers = listOf(1, 2, 3, 4, 5) // Compiler infers List<Int> }
9. Extension Functions
In Java, if you want to extend the functionality of a class, you generally use inheritance or a design pattern such as Decorator. For instance, in the example below, the Car class extends the Vehicle class and provides a specific implementation of the startEngine() method, along with an additional method drift().
// Base class representing a general vehicle class Vehicle { void startEngine() { System.out.println("Starting the engine of a generic vehicle."); } } // Derived class representing a specific type of vehicle (Car) class Car extends Vehicle { @Override void startEngine() { System.out.println("Starting the engine of a car. Ignition initiated."); } // Additional method specific to cars void drift() { System.out.println("Performing a drift in the car!"); } }
In Kotlin, you can achieve similar functionality without the need for traditional inheritance. Kotlin introduces the concept of extension functions, allowing you to add new functions without having to inherit from the class or use design patterns such as Decorator.
10. Smart Casts
Java is a statically typed language, meaning that the type of a variable is known at compile-time. This also means that as a developer you have to be mindful of how you handle type-related operations. You can do this through explicit type checking and casting. While these two methods provide control over types, they can also lead to potential errors. For instance, you can forget to perform the necessary checks or make wrong assumptions.
// Process strings from the user inputs List<String> processedStrings = new ArrayList<>(); for (Object userInput : userInputList) { // Check if the item is a string if (userInput instanceof String) { // Perform type casting to String String strInput = (String) userInput; // Process the string (e.g., validate, format, or store) processedStrings.add("Processed: " + strInput); } }
Kotlin, on the other hand, allows you to perform type checks to check the type of an object at runtime. You can use the is operator or its negated form !is to perform a runtime check that identifies whether an object conforms to a given type. Once you have checked the types using an is or ! is, Kotlin provides the Smart feature that enhance type safety and readability, reducing the need for explicit type casting.
fun processItems(items: List<Any>) { for (item in items) { if (item !is String) { // Skip items that are not strings continue } // Inside this block, 'item' is automatically cast to String println("Length of String: ${item.length}") } } fun main() { val mixedItems = listOf("Kotlin", 42, 3.14, "Smart Casts", true) processItems(mixedItems) }
11. Constructors
Constructors both in Java and Kotlin serve the same purpose i.e. they initialize objects. However, there are differences in how they are defined in Java and Kotlin. In Java, you don’t have default values for constructor parameters. Java doesn’t support named arguments, and you also need to create an overloaded constructor when you want a constructor with different parameters.
public class Person { private String name; private int age; // Default Constructor public Person() { // Default initialization code this.name = "Unknown"; this.age = -1; } // Parameterized Constructor public Person(String name) { // Initialization code with parameter this.name = name; this.age = -1; } // Parameterized Constructor with name and age public Person(String name, int age) { // Initialization code with parameters this.name = name; this.age = age; } // the rest of the code }
Kotlin simplifies the process of creating constructors. A class in Kotlin has a primary constructor and possibly one or more secondary constructors. The primary constructor is declared in the class header. Kotlin allows default values for parameters in the primary constructor.
class Person { // Primary constructor constructor(primaryParam: Int) { // Initialization code } // Secondary constructor constructor(primaryParam: Int, optionalParam: String) { // Initialization code } }
However, both languages support parameterized constructors and allow for the initialization of properties during object creation.
12. Ternary Operator
In Java, the ternary operator is a shorthand way of writing a simple if-else statement that returns a value. It’s also known as the conditional operator. You can use this operator to make your code more concise.
public class MaximumOfTwoNumbers { public static void main(String[] args) { int x = 5; int y = 10; // Using the ternary operator to find the maximum of two numbers int max = (x > y) ? x : y; System.out.println("The maximum of " + x + " and " + y + " is: " + max); } }
Kotlin does not have a ternary operator. Instead, it encourages you to use the if expression as a more versatile and readable alternative to the ternary operator we use in Java.
fun main() { val x = 5 val y = 10 // Using the if expression to find the maximum of two numbers val max = if (x > y) x else y println("The maximum of $x and $y is: $max") }
13. Primitive Types
Java has 8 primitive data types: int, byte, short, double, float, boolean, char, and long. Unlike objects, primitive types are not instances of classes, and they don’t have methods. They are stored directly in memory, can be accessed faster, and are more memory-efficient than objects.
public class PrimitiveTypesExample { public static void main(String[] args) { // Integral types byte byteValue = 127; short shortValue = 32767; int intValue = 2147483647; long longValue = 9223372036854775807L; // Note the 'L' suffix for long literals // Floating-point types float floatValue = 3.14f; // Note the 'f' suffix for float literals double doubleValue = 3.14; // Character type char charValue = 'A'; // Boolean type boolean boolValue = true; } }
Unlike Java, Kotlin does not have primitive types. Instead, Kotlin treats all types as objects, including those that are traditionally considered primitive types in Java. This is part of Kotlin’s effort to provide a more consistent and concise type of system.
fun main() { val intValue: Int = 42 val doubleValue: Double = 3.14 val boolValue: Boolean = true println("intValue: $intValue") println("doubleValue: $doubleValue") println("boolValue: $boolValue") }
14. String Templates
String templates allow you to directly embed template expressions within string literals. Template expression in this case refers to pieces of code that are evaluated and whose results are concatenated into the string. In Kotlin a template expression starts with a dollar sign ($) and consists of either a name or an expression in curly braces.
val i = 10 val s = "digma" println("i = $i") println("$s.length is ${s.length}")
On the other hand, Java relies on string concatenation or the String.format method for similar purposes. With the release of JDK 21, string templates have also been added to Java as a preview feature. This feature will now allow us to embed expressions into strings, enhance the readability of expressions that mix text and expressions, and generally make it easier to express strings that include values computed at run time.
int x = 10, y = 20; String s = STR."\{x} + \{y} = \{x + y}"; | "10 + 20 = 30"
15. Operator Overloading
Kotlin allows you to define how operators behave for instances of your classes. This is what we call operator overloading. This makes your code more expressive and allows you to work with custom types in a way that feels natural. To overload an operator, you need to mark the corresponding function with the operator modifier.
data class Coordinate(val x: Int, val y: Int) { // Overloading the plus operator operator fun plus(other: Coordinate): Coordinate { return Coordinate(x + other.x, y + other.y) } // Overloading the unary minus operator operator fun unaryMinus(): Coordinate { return Coordinate(-x, -y) } // Overloading the equals operator override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Coordinate) return false return x == other.x && y == other.y } // Overloading the times operator for scalar multiplication operator fun times(scalar: Int): Coordinate { return Coordinate(x * scalar, y * scalar) } // the rest of the code }
Java does not directly support operator overloading as is the case with Kotlin. You can only achieve similar functionality through conventional method naming and usage. However, this does not encourage conciseness and may lead to a more verbose code.
public class Coordinate { private int x; private int y; public Coordinate(int x, int y) { this.x = x; this.y = y; } // Method to mimic the behavior of the plus operator public Coordinate plus(Coordinate other) { return new Coordinate(this.x + other.x, this.y + other.y); } // Method to mimic the behavior of the unary minus operator public Coordinate unaryMinus() { return new Coordinate(-this.x, -this.y); } // Method to mimic the behavior of the equals operator @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Coordinate other = (Coordinate) obj; return x == other.x && y == other.y; } // the rest of the code }
16. Wildcard-types vs. declaration-site variance and type projections
In Java, wildcards allow you to represent an unknown type or a family of types in a generic class, interface, or method. There are three main types of wildcards: unbounded wildcards (?), upper-bounded wildcards (? extends T), and lower-bounded wildcards (? super T).
List<?> list1; // Unbounded wildcard List<? extends Number> list2; // Upper-bounded wildcard List<? super Integer> list3; // Lower-bounded wildcard
Kotlin provides an alternative approach to handling generic type variance compared to Java’s use of wildcard types. Instead of using wildcard types, Kotlin uses declaration-site variance and type projections. This approach provides more control and expressiveness in dealing with variance. Declaration-site variance in Kotlin is expressed using out for covariance and in for contravariance.
interface Box<out T> { fun get(): T }
interface Box<out T> { fun get(): T }
Kotlin’s approach to variance with declaration-site modifiers (out and in) and type projections provides a more powerful and concise way because variance modifiers are specified once on the declaration of an interface or class.
17. Cross and multiplatform development
Java and Kotlin both run on the Java Virtual Machine (JVM), which enables cross-platform capabilities in both languages. However, their approaches and tooling for cross-platform development may differ.
Java applications are compiled into bytecode, which can run on any device with a compatible JVM. Java’s platform independence allows us to write code on one platform (e.g., Windows) and run it on another (e.g., Linux) without modification.
For Android development, Java runs on Android through the Android Runtime (ART), which is the default runtime environment used in modern Android devices. Android originally used the Dalvik Virtual Machine (DVM) before transitioning to ART, starting with Android 5.0 (Lollipop).
Kotlin introduces the Kotlin Multiplatform (KMP) feature, which goes beyond the traditional JVM-based cross-platform capabilities. KMP allows us to share code between Android, iOS, and JavaScript platforms.
Kotlin takes a more modern approach with KMP, allowing us to share code between iOS and Android apps and write platform-specific code only when you need to implement a native UI or work with platform APIs.
Conclusion
In this article, we have delved into the differences between Java and Kotlin. Java remains a popular and established language for many developers, while Kotlin presents itself as a rising star with modern features. Notably, Kotlin has received endorsements from industry leaders such as Google and Netflix, propelling it to the forefront of developer preference.
While both languages share similarities and are rooted in the Java Virtual Machine (JVM), Kotlin introduces powerful features and concise syntax that enhance developer productivity and code readability. Embracing these differences can empower you as a developer to make informed decisions and leverage the strengths of each language.
Download Digma for free: Here.
FAQ
What is the primary difference between Kotlin and Java?
Kotlin is a statically typed programming language developed by JetBrains, designed to be fully interoperable with Java. The main difference lies in syntax, and conciseness, among other modern language features.
Why would I choose Kotlin over Java?
Kotlin offers more concise syntax, null safety, extension functions, and other modern features that enhance developer productivity. It also has better support for functional programming.
Is Kotlin fully interoperable with Java?
Yes, Kotlin is 100% interoperable with Java. This means you can use Kotlin and Java code together in the same project, facilitating a smooth transition for existing Java projects.
Can I migrate my existing Java code to Kotlin?
Yes, Kotlin is designed to be interoperable with Java, allowing for a gradual migration. You can start by converting individual files or classes and progressively migrate the entire codebase.
Is Kotlin used in Android development?
Yes, Kotlin is the preferred language for Android development. Google announced official support for Kotlin in 2017, and many Android projects have since migrated or adopted Kotlin as their primary language.
This post contains several inaccuracies. Specifically, the examples provided are based on Java 8, not Java 17. For instance, Java 17 supports List.of(“value1”, “value2”, “value3”), incorporates the var keyword for type inference similar to Kotlin’s val, and introduces records.
Moreover, the mentioned benefits of switching to Kotlin seem relatively minor. Aside from null safety which, in practice, is not entirely foolproof I find no compelling feature in Kotlin that would justify its adoption over Java. (This is of course my opinion).
Many snippets in java are written with old java, like:
Type inference: Since java 11 you can use the keyword var to infer types in compile time.
var names = new ArrayList();
You also used and old fashioned java sintaxys to add values in a Collection. You may use this:
val list = Arrays.asList(“you”, “Should”, “Learn”, “More”, “About”, “Java”);
Same in data Classes: In Java we count with the Lombok tag library that simplifies the creation of data classes, furthermore we count with the native record operator that allows the creation of data structures with the equals(), hashCode() and toString () methods all along.
I wonder 🤔 why’d you use String templates in java, a new java 21 feature but don’t take in consider all of those features I just mentioned that are available since java 11 and on
I really liked this article. I don’t think we should ignore the fact that some of the arguments made aren’t merely describing the differences; simply for the fact that you brought up project loom but not all of the other projects and submitted JEPs in the other sections that are completely relevant.
Chief among these being that Java does have records as data classes, It does have type inference with the var keyword. And it is adding new value types upcoming. This seems to happen far too often than we like to admit.
Other than that it was a really good read.
Talking about Java 21 but not talking about List.of() nor record…
Although the author mentions the article is not about picking sides, each bullet starts with java does this, kotlin does/encourage you to/offers way to do things better. Not a java guy but this looks like very biased.
Good article. Some of the input you received from the community regarding Java was incorrect (or maybe out of date) or needs further elaboration.
4. The Java example for building a list can be written as:
var list = List.of(“Java”, “is”, “verbose”);
Pretty much the same as Kotlin (though I would like to Java adopt Kotlin’s “val”).
5. Checked exceptions are not a problem. In fact, they are absolutely called for in situations in which an extension flow of a use case calls out an exception, like an ATM app dealing with an insufficient funds problem. That should be a checked exception. The only issue with checked exceptions is that Java decided that exceptions like IOException and SQLException, to name a few, should be checked. That was a bad decision. In the vast majority of cases, an app won’t be able to recover from those problems, so those exceptions should have been modeled as unchecked.
7. Java has records as of version 14, so we can do this:
public record Person(String name, Integer age) {}
This accomplishes the same thing as the Kotlin data class. However, Java’s records are immutable (to the extent that things can be immutable in Java). In Kotlin, data classes can be mutable and hidden state can be added. That was an egregious modeling error. Records and data classes are effectively transparent state vectors, which IMO conveys the additional semantic obligations of immutability and no hidden state, which Java enforces.
8. Java does support type inference for generics. We can do this:
var numbers = List.of(1, 2, 3, 4, 5);
which creates an immutable list of integers.
10. The Java casting example can be written a bit more more concisely (as of Java 14):
for (Object userInput : userInputList) {
if (userInput instanceof String strInput) {
processedStrings.add(“Processed: ” + strInput);
}
}
I really would like to see Java adopt null safety and operator overloading.