Compared with Python

I am learning Java as a Python developer. Here are things I am surprised by or that I find noteworthy in terms of comparing the two:

  • Needing the name the file the same as a public class in Java is a bit weird
  • Typing System.out.prinln every time is tiring
  • Overall, though, it has felt very familiar.
  • I was surprised to see how Java runs, given that it is a “compiled” language. Indeed, technically it is, since the JVM runs bytecode, but as far as my experience goes writing some short scripts, it doesn’t feel all that different form Python.
  • So far, the static typing has not been too much of an issue. In fact, I quite like being forced to have static types that don’t change randomly. As it is I used pydantic and the like a lot in Python, so having this naturally is really good.
  • On the other hand, some of the decisions surprise me. Implicit rounding when dividing integers is wild.
  • It will take me some time to get used to not being able to simple rename a variable.
  • It is cool that I can initialize empty variables with a specific type.
  • No truthy or falsy numbers.
  • Strings cannot be compared directly see Object comparisons

Java boilerplate

Part of the boilerplate required for a Java program is writing a public class whose name matches the file being run. For example, in example.java we need to start the code with

public class Example {
	public static void main(String[] args) {
		System.out.println("Hello, World!");
	}
}

The execution of the program starts from the line that follows - public static void.... The line starts and ends with {}.

Reading input

In Java, keyboard input can be accessed by importing and instantiating java.util.Scanner:

import java.util.Scanner;
 
public class Message {
 
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
 
        System.out.println("Write a message:");
        // Write your program here
 
    }
}

This raises a few questions: to start with, the Scanner object takes an input, in this case System.in. Are there use-cases for using a Scanner object that has a different input? Perhaps it can be used to read a file, or a stream of some sort.

It is also worth noting that Scanner uses the .nextLine() method to parse each line - defined by a carriage return or \n. However, there is also a .nextInt() method; this raises the question of how to read a mixed string: for example, suppose we want to read String input = "apple 123 banana 456"; To manage this, we use the .hasNext() method inside a loop:

import java.util.Scanner;
 
public class MixedInput {
    public static void main(String[] args) {
        String input = "apple 123 banana 456";
        Scanner scanner = new Scanner(input);
 
        while (scanner.hasNext()) {
            if (scanner.hasNextInt()) {
                int number = scanner.nextInt();
                System.out.println("Found an integer: " + number);
            } else {
                String word = scanner.next();
                System.out.println("Found a string: " + word);
            }
        }
        scanner.close();
    }
}

It is also worth noting that if we are using or expecting user input via System.in, using .hasNext() will be blocking the code execution, thereby pausing it and allowing users to add input to their heart’s content. Pressing “Enter” without any specific input will send \n to the program, which will then be printed. To actually end the input loop we need to send an EOF Signal (Ctrl-D).

We can also typecast input strings into things like integers (assuming the string itself is appropriate), using Integer.valueOf. For example:

String stringValue = "42";
int intValue = Integer.valueOf(stringValue);
System.out.println(intValue); // Prints the integer 42

Other available conversions are Boolean.valueOf, Double.valueOf. Note that with a boolean, true and TrUE (case insensitive) will be evaluated to true.

There are a few more intricacies here than I care to get into at the moment, but as always, the Java documentation is a good place to start.

Variable assignments

In Java variables can be initialised on their own without having an explicit assignment; in all cases, the first time a variable is declared the type of that variable must be included. For example:

String myVar;
double floatVar;
int intVar;

Note that following assignments of the variable do not require a type declaration:

String myVar;
String myOtherVar = "Good bye";
myVar = "Hello";
myVar = myOtherVar;

However, once a variable is declared to be of a particular type, we cannot declare it again with another (or the same) type:

String myvar;
double myvar; // This will fail
//----
String myvar = "Hello";
String myvar = "Good bye"; // This will also fail

Note

There is a case in which a variable can be type-cast into another one automatically: an int can be assigned to a variable of type double, although the actual type will remain double. A double cannot be assigned to a variable of type int:

double floatingPoint = 4.2;
int intNumber = 1;
floatingPoint = intNumber;
// This works, but floatingPoint ends up with 1.0 as its value.

Statements and expressions

We can evaluate an expression as part of the argument for a function, for example:

System.out.println(2+2); // Prints 4

However, if we are combining strings with the expression we must take care to enforce proper parenthetical orders of operation. Consider these two cases:

System.out.println("Four: " + (2 + 2)); // Prints "Four: 4"
System.out.println("But! Twenty-two: "+ 2 + 2) // Prints "But! Twenty-to: 22"

Another interesting aspect (for us Python programmers!) is that integer division in Java will still product an integer. For example:

int result = 3 / 2;
System.out.println(result); // Prints 1
// -----
int first = 3;
int second = 2;
double result = first / second;
System.out.println(result); // Also prints 1

However, if either the dividend or the divisor is a floating point number, the result will also be a floating point number:

double whenDividendIsFloat = 3.0 / 2;
System.out.println(whenDividendIsFloat); // Prints 1.5
 
double whenDivisorIsFloat = 3 / 2.0;
System.out.println(whenDivisorIsFloat); // Prints 1.5

If we want to ensure that our divisions work as expected, we can typecast integers into double:

int first = 3;
int second = 2;
 
double result1 = (double) first / second;
System.out.prinln(result1); // Now it prints 1.5
 
double result2 = first / (double) second;
System.out.println(result2); // Also prints 1.5
 
double result3 = (double) (first / second);
System.out.println(result3); // Prints 1.0. The division happens first, so the value that is typecast is 1 -> 1.0
 
int integer = 3.0 / 2; // This will fail due to the implicit conversion
 
int integer = (int) 3.0 / 2;
System.out.println(integer); // Prints 1, since the values are all ints
// Note that if the variable was `double integer = ...` it would print 1.0
 
int divident = 3;
int divisor = 2;
double result = 1.0 * divident / divisor;
System.out.println(result); // Prints 1.5, since the divident is converted into a floating point by multiplying it with a floating-point number prior to division
 
int dividend = 3;
int divisor = 2;
double result = dividend / divisor * 1.0;
System.out.println(result); // On the other hand, this prints 1.0. The multiplication and division have the same precendense, so in this case they are read left-to-right; the division is performed between integers and then converted to a double.

Conditionals

A conditional is constructed by using if and an expression in parenthesis:

if (expression) {
	// Some code
}

Return values

In Java methods, the type that a function will return is defined in the signature of the method. Note that in the return keyword we don’t have to declare the type again, since it is obvious from the signature.

Caution

As in the examples below, the type that a method returns can be typecasted!

public static double returnsDouble() {
    return 10;  // works: 10 (int) is promoted to 10.0 (double)
}
 
public static short returnsShort() {
    return 10;  // works: 10 fits in a short
}
 
public static byte returnsByte() {
    return 128; // ERROR: 128 doesn’t fit in a byte
}
 
public static String returnsString() {
    return 128; // ERROR: 128 won't convert to a str
}
 

ArrayList

An ArrayList is a resizeable object that has the List interface. It is used by declaring its type between < and > after declaring an ArrayList:

import java.util.ArrayList;
 
ArrayList<String> myList = new ArrayList<>();

This is done because the first part, before the =, is the declaration for the type system. However, at this point no actual object exists yet. To do that, we instantiate it with new ArrayList<>()1.

To set a value at a specific index of an ArrayList, use the set(<index>, <value>) method.

We can iterate over an ArrayList with a for-each loop, which is akin to a Python for x in y loop:

ArrayList<String> teachers = new ArrayList<>();
 
teachers.add("Simon");
teachers.add("Samuel");
teachers.add("Ann");
teachers.add("Anna");
 
for (String teacher: teachers) {
    System.out.println(teacher);
}

Note

The reason we declare the type of the variables in the loop despite knowing that the ArrayList is of type String in this case is because we can have mixed type lists at the expense of type safety by doing ArrayList<Object>.

We can remove values from an ArrayList with the .remove method. The method will remove a value by index if passed an int, i.e. list.remove(1). Alternatively, it will remove an item by value if it is in the structure, i.e. list.remove("Remove this string"). Note that if we are trying to remove an Integer by value, we have to do list.remove(Integer.valueOf(1)).

Note

ArrayList parameters passed to methods are reference-types, meaning that unlike primitives, the method modifies the list itself (and thus make modifications to a variable declared outside the scope of the method).

Arrays

Arrays are the ancestor of ArrayList, and have a fixed length (unlike ArrayLists, which have a dynamic size). To create an array of a type, do the following:

int[] numbers = new int[3];
String[] strings = new String[5]

Unlike ArrayList, we can set values based entirely on the index of the element: numbers[1] = 1;.

We can also avoid declaring “new” and the length of the array by simply declaring it’s values between {} curly braces:

int[] numbers = {1, 2, 3, 4, 5}
String[] strings = {"a", "b", "c"}

Constructor overloading

In Java we can have multiple constructors for the same class, provided they do not have the exact same parameters. Thus we could have a Person class with the following:

public Person(String name) {
        this.name = name;
        this.age = 0;
        this.weight = 0;
        this.height = 0;
    }
 
public Person(String name, int age) {
    this.name = name;
    this.age = age;
    this.weight = 0;
    this.height = 0;
}
 
// Can be used with:
 
public static void main(String[] args) {
    Person paul = new Person("Paul", 24);
    Person ada = new Person("Ada");
 
    System.out.println(paul);
    System.out.println(ada);
}

Of course, this can then be improved to make the code DRY:

public Person(String name) {
    this(name, 0);
    //here the code of the second constructor is run, and the age is set to 0
}
 
public Person(String name, int age) {
    this.name = name;
    this.age = age;
    this.weight = 0;
    this.height = 0;
}

Note that the shorter constructor is a special case of the second constructor. The notation this(name,0); is a bit weird, but what it does is it uses the arguments in the general constructor in order. In other words, it is saying for the first argument, use the current value of name. For the second arg, use 0.

It is, in Python terms, doing *args, of sorts.

This same overloading can be applied to methods.

Primitives vs references

Java has eight different primitive variables. These are: boolean (a truth value: either true or false), byte (a byte containing 8 bits, between the values -128 and 127), char (a 16-bit value representing a single character), short (a 16-bit value that represents a small integer, between the values -32768 and 32767), int (a 32-bit value that represents a medium-sized integer, between the values -231 and 231-1), long (a 64-bit value that represents a large integer, between values -263 and 263-1), float (a floating-point number that uses 32 bits), and double (a floating-point number that uses 64 bits).

A reference is always an object. If we create a class without overriding toString we will see the reference to that object. This is a lot like Python without __str__ methods.

Note

Another important detail here is that primitives are stored on the stack, whereas references/objects are stored on the heap.

Yet the most significant difference between primitives and reference variables is that the former are immutable. The internal state of reference variables can typically be mutated; this is because the value of a primitive variable is stored directly in the varible assigned to it:

int number = 0;
int number1 = number;

In both cases, the 0 value is copied to the variable number (either directly, or by copying the stored value of number into number1).

On the other hand, reference variables simply hold a reference to the internal state of the object they refer to.

Interfaces

An interface in Java is a contract that specifies what a class can do. That is to say, a class that implements a specific interfaces promises to have the methods listed by the interface, but will implement the specific logic separately. This is different from abstract classes, which are designed for inheritance rather than implementation.

The primary difference is that an inherited class suggests that all subclasses are closely related to the superclass. For instance, Animal can be inherited by Cat, Dog, etc - they are all types of animal. However, we can have all of them implement the move interface, which in addition to being true of all Animals might also apply to Insects or other things.

Note

Abstract classes and inheritance are used when the subclasses are closely related and/or share the same functionality or data. Interfaces, on the other hand, are useful with different or dissimilar classes share common functionality.

Note that like inheritance, interfaces affect the type of the class being designed. That is to say:

public class Document implements Readable {
	...
}
 
public class Billboard implements Readable {
	...
}
 
ArrayList<Readable> stuffRead = new ArrayList<>();
stuffRead.add("Billboard ad");
stuffRead.add("A document");
 
for (Readable readable : stuffRead){
	// Do stuff
}

The particular usefuless of this is that it serves as a way to ensure we implement. For example, we can use the interface type as a parameter to a method:

public class Printer {
	public void print(Readable readable) {
		System.out.println(readable.read());
	}
}

This means that any class that implements the Readable interface can be given to Printer.print() as a parameter. It also means we can construct arrays of objects that implement that specific functionality. This is an example of the Liskov Substitution Principle (LSP)

It is also particularly useful when using Factory patterns, since we can do something like this:

import java.util.Random;
 
public class Factory {
    public Packable produceNew() {
        // The Random-object used here can be used to draw random numbers.
        Random ticket = new Random();
        // Draws a number from the range [0, 4). The number will be 0, 1, 2, or 3.
        int number = ticket.nextInt(4);
 
        if (number == 0) {
            return new CD("Pink Floyd", "Dark Side of the Moon", 1973);
        } else if (number == 1) {
            return new CD("Wigwam", "Nuclear Nightclub", 1975);
        } else if (number == 2) {
            return new Book("Robert Martin", "Clean Code", 1);
        } else {
            return new Book("Kent Beck", "Test Driven Development", 0.7);
        }
    }
}
public class Packer {
    private Factory factory;
 
    public Packer() {
        this.factory = new Factory();
    }
 
    public Box giveABoxOfThings() {
         Box box = new Box(100);
 
         int i = 0;
         while (i < 10) {
             Packable newThing = factory.produceNew();
             box.add(newThing);
 
             i = i + 1;
         }
         return box;
    }
}

Quote

Using interfaces in programming enables reducing dependencies between classes. In the previous example the Packer does not depend on the classes that implement the Packable interface. Instead, it just depends on the interface. This makes possible to add new classes that implement the interface without changing the Packer class. What is more, adding new Packable classes doesn’t affect the classes that use the Packer class.

Object comparisons

Consider the following code:

Scanner reader = new Scanner(System.in);
 
System.out.println("Enter the first string");
String first = reader.nextLine();
System.out.println("Enter the second string");
String second = reader.nextLine();
 
if (first == second) {
    System.out.println("The strings were the same!");
} else {
    System.out.println("The strings were different!");
}

If both input strings are “hello”, what will the program print? That’s right… they are different.

But ok, fair enough I guess; strings cannot be compared directly. So instead, we use a string method for comparison, such as first.equals(second).

Cool. But consider this case:

String string1 = "hello";
 
String string2 = "hello";
boolean same = string1 == string2 

Surely that would be the same as the first example right? No. In this case, the variable same evaluates to true. This is because the Java Virtual Machine has an optimization to check if a string already exists in memory; if it does not, then string1 will point to a memory address (say, @123) , which will store the characters for “hello”. When the next assignment occurs in string2, the JVM will again look for that string in memory. Since it is found at @123, string2 will also point at it. As a result, both strings evaluate to true.

This can be modified with:

String string1 = new String("hello");
 
String string2 = new String("hello");
boolean same = string1 == string2 

In this version we explicitly create new string objects, meaning that the JVM will not attempt a look up at existing memory addresses. Thus, this version will be false.

So what did I learn? That the comparison operator == compares the memory address of two objects, not whether the values in those objects are the same.

Footnotes

  1. Post-Java 7, <> is called a Diamond Operator. It tells Java to use the types in the declaration and use them in the constructor. It is equivalent to the also valid `ArrayList myList = new ArrayList();