String param = "expected";
if (param == "expected") {
System.out.println("These strings are equal!")
}
02 April 2024, Joachim Schirrmacher
My journey from TypeScript to Java - Part 1
My journey from TypeScript to Java - Part 1
For several years now, I’ve been programming in Typescript and enjoyed it significantly due to its simple and yet powerful type safety system in contrast to JavaScript.
However, a lot of people in DB Systel use Java as their preferred programming language. To understand what they like about Java, but also because it is easier to maintain the software in the team, I wanted to learn Java myself.
My learnings on my way from TypeScript to Java might be useful for others going the same way. While I’m going this way, I like to add more of these articles. For now, I start with some beginner problems, but I already have prepared some more!
-
Part 1
-
Part 2 (planned)
-
Optional values and defaults
-
Objectifying mappings
-
-
part 3 (planned)
-
Enum-erable charm
-
Table manners for databases
-
Spice up your functions with curry
-
Many thanks to my friends who helped me with the formulation of my travelogue and showed me new ways:
-
Caroline Rieseler
-
Jasper Gerigk
No some objects are equal
The first thing I stumbled upon might be a real beginner problem.
I expected that I could compare strings very similar to TypeScript with the equality operator.
Sure, the ===
operator in TypeScript is very special, but the IDE stops you from using it in Java.
However, it doesn’t stops you to use the 'normal' ==
equality operator for strings.
Since String
is a class, and classes are only equal, if they are the same object, equality on two Strings normally doesn’t work.
You might already know this behavior from TypeScript when, e.g. comparing `Date`s.
To make things more complicated, there are situations where comparisons work nevertheless:
This happens because Java re-uses string literals like 'expected' in this example.
So, the param
object and the "expected" string literal are actually equal!
However, if the String is created dynamically (e.g. from user input), it doesn’t:
String param = " expected ".trim();
if (param != "expected") {
System.out.println("These strings are not equal, even if they have the same content!")
}
In Java, comparing of objects is done with an equals()
method:
if (param.equals("expected")) {
System.out.println("Now the equality is recognized")
}
However, on primitive types, equality is implemented just like I would expect.
So a == 5
works if a
is an int
.
And, surprisingly, if you see a variable defined as Integer a
, you can use a == 5
, though Integer
is a class and a
therefore is an object of this class.
Actually, the ==
operator defined here overloads the standard one, so the 'normal' rule of non-equality of objects if they are not the same is not applicable.
Try it out yourself:
class Test {
public static void main(String[] args) {
Integer a = Integer.parseInt(args[0]);
if (a == 5) {
System.out.println("a is 5");
} else {
System.out.println("a is not 5");
}
}
}
In my opinion, this is not very intuitive, but that might be due to my past in TypeScript.
Regrettably, Java implements function overloading only in a few cases (like the above) and doesn’t give us the opportunity to define them ourselves.
Else, I would create a MyString
class and define a ==
method to compare strings like numbers.
Fun with null values
Typescript inherits the sometimes difficult distinction between undefined
and null
from Javascript.
Some people might think, this is not really necessary, but the two actually have a slightly different meaning: while null
means that I actually assigned a kind of empty value
to a variable or parameter, undefined
means 'not set'.
null
is rather empty on purpose.
Java doesn’t make such a distinction, it only has null
- which might be sufficient for most use cases.
But how does both languages handle such values?
Let’s say, you have a variable myObject
which might be an object having a function value()
but also may be undefined
.
The function might return a Date or again undefined
.
How can I compare this with the current time?
In Typescript this can be done with the so called 'Optional Chaining' like this:
if (myObject?.value()?.getTime() > Date.now()) { // ... }
The ?
means that when we get an undefined
, just stop evaluating, returning undefined
as the result of the expression.
TypeScript programmers know this as 'falsy', which means that it something that is similar to the implementation of false
.
Falsy in TypeScript are the values 0
, undefined
, null
, ""
(the empty string), and, of course, false
.
This is a very compact and, if you understand the syntax, readable form.
In Java, there is no such thing as a optional chaining ?.
operator, we have to check every possible null value.
Also, Java uses different types for numbers, int
or long
, so we need to decide, what value()
should return.
If we want to use null
as well, both types won’t allow that.
But there is an alternative: instead of using the native int
or long
types, use the classes Integer
or Long
(with capital first letter) for the return value of value()
instead!
They may be null (as all objects).
if (myObject != null
&& myObject.value() != null
&& myObject.value() > Long.valueOf(Instant.now().getEpochSecond())) {
// ...
}
Why the hell do I need to specify types?
As you saw in my previous examples, Java requires you to specify the data type when defining a variable. While in Typescript, a definition with a value looks like
const string = functionThatReturnsAString();
in Java, it requires an additional specification
String string = functionThatReturnsAString();
This gets particularly strange, if you have a value that needs to be explicitly converted to a string:
String string = functionThatReturnsAnObject().toString();
When the function is already specifying a return type, specifying the type of the variable seems to be just overhead. The compiler could just infer the type automatically!
But to improve the situation in Java 10 and higher, instead of the data type, one can use the var
keyword (which is rather a reserved type name, to be exact), so that it looks similar to TypeScript:
final var string = functionThatReturnsAnObject().toString();
The Java compiler then also automatically infers the actual type.
In fact, TypeScript also has a var
keyword, though I never would use it, and instead only use const
or - in rare cases - let
.
Differentiating between variables that can change later (let
) and those which may not be changed (const
- an immutable value) is a very useful feature to avoid unwanted changes.
Union types
A nice feature of TypeScript are Union Types. They allow to combine multiple types to be used in a clear way:
type Fruit = "apple" | "orange" | "banana";
type DairyProducts = "milk" | "butter";
type Food = Fruit | DairyProducts;
There is no such thing as Union Types in Java.
In Java, you would instead use enum`s for defining `Fruit
and DairyProducts
.
@AllArgsConstructor
public enum Fruit {
APPLE("apple"),
ORANGE("orange"),
BANANA("banana");
String name;
}
The upper case identifiers work as constants, the string literals in the braces the values of these constants.
The field name
is needed to hold the value in each of the three instances of Fruit
.
Fruit values can be accessed like this:
Fruit fruit = Fruit.BANANA;
However, it is not easily possible to combine such enums, because they are compiled to constants.
Instead, you would use interfaces and let the Fruit
and the DairyProduct
enums implement this interface.
public interface Food {}
@AllArgsConstructor
@Getter
public enum Fruit implements Food {
APPLE("apple"),
ORANGE("orange"),
BANANA("banana");
String name;
}
@AllArgsConstructor
@Getter
public enum DairyProduct implements Food {
MILK("milk"),
CHEESE("cheese");
String name;
}
Now you can use the new Food
interface as the type to collect both, `Fruit`s and `DairyProduct`s together:
List<Food> food = List.of(Fruit.BANANA, DairyProduct.CHEESE);
Note that it is not possible (as far as I know) to use Food
as the qualifier for BANANA
and CHEESE
and that it is not that easy to use the lower case equivalents for assignments.
Instead, one would need to iterate over the enum values and find
the requested value text.
I use Lombok to not have to implement getters and constructors. They are necessary to have the lower case values at least when using the enum for writing to a database or when generating JSON.
Also note that every interface and class needs a separate file in Java.
All in all, `enum`s seems to be a very cumbersome feature. But this is also valid for TypeScript, where I prefer to use Union Types.
Combine object structures
To combine object structures in TypeScript you would use the &
:
type Person = {
name: string;
email: string;
};
type AuthDetails = {
username: string;
password: string;
};
type User = Person & AuthDetails;
In Java, you would rather define two interfaces, and define a class implementing both. This would work in Typescript as well. However, interfaces in TypeScript can only be used to describe object strucures, not primitives. Read more about the differences of type aliases and interfaces in the official TypeScript documentation.
I’m still working with Java, so stay tuned to read more experiences in the next chapters of my journey, when I will cover optional values, defaults and mapping of objects.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.