A more detailed introduction to Java

We will now review basic features of Java in a little bit more detailed manner in order to help students without prior Java experience to dive straight into Java programming. The course assumes that students already have basic programming skills in e.g. Python or C++.

The Java syntax has been borrowed almost directly from C/C++, and also Java is a statically typed language. These similarities make it quite easy to migrate to Java programming if you have prior experience in C or C++.

Java types

Java types fall broadly into two categories: primitive type and reference types. Primitive variables are variables whose types are primitive types, and reference variables are variables whose types are reference types.

Primitive variables store simple numbers and truth values and are similar to C/C++ value type variables: a primitive variable in itself stores a value.

Java has the following primitive types:

  • Integer types: byte, short, int, long ja char.

    • The first four are 8-, 16-, 32- and 64-bit signed integer types that use two’s complement representation.

    • char is a 16-bit unsigned integer type intended for representing UTF-16 encoded unicode characters.

    • Java integer constants (e.g. 100) have type int. If an integer constant should be treated as a long value, add ‘L’ to its end (e.g. 3123456789L).

  • Floating point types: float, double

    • Floating point types using 32- and 64-bit IEEE 745 -standard representations, respectively.

    • Floating point constants (e.g. 3.14) have type double. If a floating point constant should be treated as a float value, add ‘F’ to its end (e.g. 3.14F).

  • Truth value type: boolean

    • Two possible values: true or false.

Especially those with C/C++ background should take note that the Java standard defines fixed sizes and representations for the primitive types. Hence Java does not suffer from the obscurity of C/C++ regarding e.g. what kind of value range a certain integer type can store in different execution environments.

The following code snippet gives some examples about both legal and illegal primitive variable definitions in Java. Note e.g. how Java is more strict than C/C++ with regards to type conversions and constants. If you wish to try to e.g. compile this code on your computer, you can e.g. copy-paste the example code inside the main function of the previous Netbeans code skeleton example (but note that the code will produce compilation errors as described in the code comments).

int i = 1000;        // int variable i = 1000.
int j;               // int variable j = 0 (numbers are default initialized to 0).

long k = 3123456789;   // COMPILER ERROR: 3123456789 is too large to be an int!
long m = 3123456789L;  // OK: 3123456789L is a long constant and fits into a long variable.

byte a = 100;        // byte variable a = 100. int constant 100 --> byte value 100.
byte b = i;          // COMPILER ERROR: value changing conversion int --> byte.
                     // Java converts an int constant into a smaller integer type IF
                     // the smaller type can represent the value. i = 1000 does not fit
                     // into a byte and the actual result would be a byte value -24.
byte c = (byte) i;   // OK: an explicit type cast int --> byte which results in c = -24.
                     // Java type cast uses the C syntax: the target type is given
                     // in parentheses in front of the value we want to convert.

boolean e;                // boolean variable e = false (default initialized to false).
boolean f = true;         // boolean variable f = true.
boolean g = (boolean) i;  // COMPILER ERROR: int i cannot be converted into a truth value.
                          // Different from C/C++/Python: in Java truth values and integers
                          // cannot be converted from/to each other!
boolean h = i > j;        // h = true because the condition i > j, ie. 1000 > 0, is true.

float x = 0.0;       // COMPILER ERROR: 0.0 is a double constant and Java does not convert
                     // a double into a float without explicit type cast.

float y = 1.5F;      // OK: 1.5F is a float constant.
float z = 0;         // OK: Java automatically converts int 0 --> float 0.0F.

double p = 3.14;
float q = p;          // COMPILER ERROR: double cannot be assigned to float!
float r = (float) p;  // OK: explicit type cast double --> float.

int s = p;           // COMPILER ERROR: double cannot be assigned to int!
int t = y;           // COMPILER ERROR: float cannot be assigned to int!
int u = (int) y;     // OK: explicit type cast float --> int. The result is u = 1, which
                     // is simply y = 1.5 without the fractional part.

Reference variables hold object references. Reference variables are similar to C/C++ pointer variables but require no special pointer syntax, and Java does not permit references based on memory addresses. Python’s variables are similar to Java’s reference variables; a major difference is that Python’s variables do not have static types.

A variable x of type T defined as T x is a primitive variable if and only if T is one of the primitive types listed below. Otherwise x is a reference variable that can refer to objects of type T.

A Java object is either an array or an object (instance) of some class; note that in Java all arrays, including ones that store primitive values, are objects. In principle all Java objects are created dynamically (usually with the new operator). In some cases, such as when defining strings and arrays, the object can be defined in a more direct manner using an initialization syntax similar to one used in C.

A Java reference always either refers to an existing object or holds a so-called null reference (which means that the variable does not point to anything). Given a reference variable x we often for simplicity say that “x is …” when we acually mean “x refers to …”.

An important difference between C/C++ and Java is that Java uses automated memory management (garbage collection). Programmers do not need to worry about releasing dynamically allocated objects from memory: the Java virtual machine (JVM) can automatically release abandoned objects (objects that are not referenced anymore) in order to reuse the memory for storing new objects.

A member of an object (or a class/interface) is referred to with a similar dot-operator as in e.g. C/C++ and Python. For example if x is a reference variable, then x.y``refers to the member ``y of the object x refers to, and this operation is naturally legal only if the object has a member named y. If x is a primitive variable, an attempt to refer to x.y causes a compiler error. One of the most common runtime errors is caused by attempting a member reference x.y when the reference variable x currently is null.

Java contains a standard class library (called the Java API), which is much more comprehensive than the C++ STL library and somewhat comparable to the Python Standard Library. Some of the most fundamental Java API classes are e.g. the string classes String and StringBuilder and the primitive type “wrapper” classes Byte, Short, Integer, Long, Character, Float, Double and Boolean. The wrapper classes enclose a corresponding primitive type value and are useful in contexts where the manipulated values are expected to have reference types. The wrapper classes also contain useful functions for manipulating values of their type (e.g. conversion functions from a string representation to the primitive type and vice versa).

All above mentioned basic classes are immutable: their objects are set to express a value when the object is constructed and that value cannot be changed later.

Java simplifies the use of the primitive type wrapper classes by performing automatic type conversions bertween the primitive types and the corresponding wrapper classes. These so-called boxing and unboxing conversions are furthermore the only legal conversions between a primitive and reference type. E.g. if we attempt a type cast from a primitive type to a reference type, the conversion is legal if and only if Java would have performed the same conversion automatically as a boxing conversion. Hence there should virtually never be a need for explicit type casts between primitive and reference types.

The preceding rules have one exception: using the plus operator + may lead to converting a primitive type into a String. If one of the operands of + is a String and the other has any other type, that other operand will be automatically converted into a String. This applies to both primitive and reference types (and even null references: they are converted into the string “null”). Here an object is converted into a String by calling its member function toString(); all reference types in Java are guaranteed to have this member function (at least a default version that gives crude information about the object’s identity).

The example code given below illustrates Java’s reference variables, strings, primitive type wrapper classes and arrays.

int x = 100;       // primitive value int = 100.
Integer y = 200;   // An Integer object enclosing int = 200; created by a boxing conversion.
x + y;             // Arithmetic and logical operations can be applied to wrapper objects.
y / 3;             // E.g here x + y = 300 and y / 3 = 66.
x > y;             // Evaluates to false because the condition 100 > 200 does not hold.

String s1 = "One";                    // s1 refers to a constant String object "One".
String s2 = new String(" to three:"); // s2 is a new String object " to three:".
String n1 = " 1";
String n2 = " 2";
String n3 = " 3";

// The + operator means concatenation when applied to (at least one) string: the result is
// a new String object that represents the concatenation of the two operands. If one of
// the operands is not a String, Java will automatically convert it into a String.
// Below s3 will eventually refer to a String object representing "One to three: 1 2 3".
String s3 = s1 + s2 + n1 + n2 + n3;

// Above s3 was constructed by a chain of String concatenation operations, each creating
// new strings: "One to three:", "One to three: 1", "One to three: 1 2" and
// "One to three: 1 2 3". This is clearly inefficient: repeated concatenations involved
// copying 13 + 15 + 17 + 19 = 64 characters although the end result has only 19 characters.
// In this kind of a situation it might be a good idea to use the mutable String type
// StringBuilder instead. A StringBuilder object can be manipulated e.g. with the member
// function append that adds a given string to the end of the current string. The code
// below makes similar concatenations as above, but this time the use of StringBuilder
// avoids building a new string et each step and needs to copy only 19 characters: the first
// StringBuilder object contains "One" and will be extended with s2, n1, n2 and n3.
StringBuilder b = new StringBuilder(s1);
b.append(s2).append(n1).append(n2).append(n3);

// The contents of a StringBuilder object can be transformed into a regular immutable String
// by calling the member function toString. Here it will create a new String with 19 characters.
// Constructing the String s4 hence requires copying a total of 19 + 19 = 38 characters, if we
// take account the work required to build the StringBuilder object b above.
String s4 = b.toString();  // s4 is initialized with b's string "One to three: 1 2 3".

// It is worth noting that the compiler can concatenate string literals efficiently at compile
// time. The preceding note about possible inefficiency concerns concatenation where at least
// one operand is a String object. E.g. s5 below will be initialized directly into the string
// "One to three: 1 2 3" without excessive copying: the compiler can first stitch the string
// literals together into "One to three: 1 2 3" and then use that to initialize a String object.
String s5 = "One" + " to three:" + " 1" + " 2" + " 3";

// Note that if the basic comparison operators != and == are applied to reference variables,
// the result will tell whether the two variables refer to the same object. Java has a convention
// that a class should implement an object comparison function "equals" if the class implementor
// expects that objects of that class need to be compared for similarity.
s3 == s4;       // false bacause s3 and s4 refer to different String objects.
s3 != s4;       // true because s3 and s4 refer to different String objects.
s3.equals(s4);  // true because s3 and s4 refer to similar strings "One to three: 1 2 3".

// String literals are String objects so you can call also their member functions!
"One two three: 1 2 3".equals(s3);    // true because s3 is "One to three: 1 2 3".

// If you compare a string literal and a String object, use the above order where the equals
// function of the string literal is called: this works correctly even if the compared other
// String happens to be null!
String s5 = null;
"One two three: 1 2 3".equals(s5);  // false (and not an error!) because s5 is null.
// The same comparison but using opposite order: now the program would crash with a runtime error!
s5.equals("One two three: 1 2 3");  // Cannot call a member function of a null reference...

String s6;  // A reference variable without an explicit initializer is initialized to null.
s5 == s6    // true because both s5 and s6 are null.

// The wrapper class of each primitive type "TYPE" contains a class function named "parseTYPE"
// that interprets a string into a value of that particular primitive type. A string can e.g.
// be converted into an int value by calling Integer.parseInt, into a float value by calling
// Float.parseFloat and into a boolean value by calling Boolean.parseBoolean.
int i = Integer.parseInt("2021");                       // i = 2021.
float pi = Float.parseFloat("3.1415926535");            // pi = 3.1415926535F.
boolean t = Boolean.parseBoolean("tRuE");               // t = true (case insensitive).

// Each wrapper class also contains a class function "toString" that converts a primitive value
// given as a parameter into a String. Use these to convert primitive values into strings. If the
// original value is already wrapped, the conversion should be done more directly by calling
// the "toString()" member function .
String piStr = Float.toString(pi);           // piStr = "3.1415927"  (float is inaccurate!)
String weekHourStr = Integer.toString(24*7); // weekHourStr = "168"
String yStr = y.toString();                  // yStr = "200", here y was an Integer object.

// The Java array syntax is quite similar to C/C++: a variable definition is changed into an
// array definition of the same type by adding one pair of square brackets per array dimension.
// There are two notable differences:
//   1) the square brackets should be placed after the type and not after the name.
//      (Java does allow also the latter C-style, but this is consided bad style in Java.)
//   2) Brackets are given as empty, without size information: the array dimension sizes are
//      specified either implicitly in form of an array initializer or explicitly as part of a
//      new operator call that creates a new array.

// Define vals as a reference variable that can refer to an int array.
// Here it = null as we did not give an initializing value.
int[] it;

// Set it to refer to a new int array of size 100 created with the new operator. The size is
// given inside square brackets after the array item type. The array values are default
// initialized with zeros.
it = new int[100];

// An array can naturally be created directly when defining an array reference variable.
// This time we create an array of reference type values; they are default initialized to null.
// The result below will thus be a Double array initialized with 1000 null references.
Double[] dt = new Double[1000];

// Below the result will be dt[172] = 0.0 as it[5] = 0. Here we first need an explicit type cast
// from an int into a double so that Java can then perform an automatic autoboxing conversion
// double -> Double. E.g. an attempt to cast an int directly into a Double would lead into a
// compiler error.
dt[172] = (double) it[5];

// The initial contents of an array can be defined using similar syntax as in C/C++: list the
// comma separated array items inside curly brackets. Autoboxing is apllied also to array value
// initializers so e.g. below we may give int constants as initializer values for an Integer
// array: Java will automatically convert them to Integer objects.
Integer[] it2 = {1, 2, 3, 4};

// It is possible to use new operator explicitly with an array initializer expression. In this
// case the array size is not specified inside square brackets. Here it3 is similar to it2.
Integer[] it3 = new Integer[]{1, 2, 3, 4};

// Java arrays have an int member variable "length" that tells the size of the array.
it2.length == 4;            // true because the array it2 was initialized to contain 4 values.

// Java does not allow illegal array index references. E.g. the attempt below to increment the
// value it2[4] leads into a runtime error as the legal indices for a size-4 array are 0...3.
it2[4] += 1;

As mentioned in the comments above, objects should be compared by calling a member function equals that compares the objects in some reasonable manner and returns true if they are similar and otherwise false. You should be careful about this: if the object class does not implement its own version of the equals function, the objects are guaranteed to have at least a defautl version that works in similar manner as the ==-operaattori. That is, compares object identities instead of their similariry.

Java’s basic syntax and operations

The example program given below illustrates some basic Java syntax and operations. The code does not yet delve into properties of Java classes.

The program checks whether a date given as a command line parameter is legal. Usually we would not implement this type of a check ourselves but would instead use some readily available Java date manipulation class.

// If you wish to test this code on your own computer, copy the code into a file LegalDate.java,
// compile it as "javac LegalDate.java" and run as "java LegalDate dd.mm.yyyy", where
// missä dd.mm.yyyy is some date (e.g. 29.2.2021).
public class LegalDate {
  // Note how all members of LegalDate are static.

  // A Java function definition has a similar form as C/C++ functions: the return type is in
  // front of the function name, and the parameter types and names are listed inside parentheses
  // after the function name.
  // The function isLeapYear returs a truth value telling whether "year" is a leap year.
  static boolean isLeapYear(int year) {
    // A leap year: divisible by 4 and either not divisible by 100 or divisible by 400.
    // Java has taken arithmetic logical operation syntax quite directly from C.
    return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
  }

  // We could store information about the number of days in each month in many ways. Here, as a
  // further example of array syntax, we define a two-dimensional int array: one size-2 subarray
  // for each month that tells the number of day in a normal and leap year. Only February has
  // a difference between the two.
  static int[][] mDays = {{31, 31}, {28, 29}, {31, 31}, {30, 30}, {31, 31}, {30, 30},
                          {31, 31}, {31, 31}, {30, 30}, {31, 31}, {30, 30}, {31, 31}};

  // The function monthDays tells how many days the specified month has in the specified year.
  // A return value -1 signals illegal month number.
  static int monthDays(int month, int year) {
    int days = -1;
    if(1 <= month && month <= 12) {
      // Conditional operator same as in C/C++.
      days = isLeapYear(year) ? mDays[month-1][1] : mDays[month-1][0];
    }
    return days;
  }

  // The function isLegalDate checks whether the parameters day, month and year correspond
  // to a legal date. The year is assumed to be always legal (e.g. also negative values).
  static boolean isLegalDate(int day, int month, int year) {
    // The result is simple to compute as monthDays returns -1 if the month is illegal.
    return (1 <= day) && (day <= monthDays(month, year));
  }

  public static void main(String[] args) {
    // Java String class has a member function "split" that returns a String array that
    // contains the result of partitioning the string based of the specified delimiter.
    // Here we partition the command line parameter args[0] based on the dot character ".".
    // There are two escape characters '\' in front of the dot due to technical reasons:
    // split interprets its parameter as a regular expression where dot has a special meaning.
    String[] dayMonthYear = args[0].split("\\.");
    boolean dateOk = false;
    if(dayMonthYear.length == 3) {
      // Convert the string parts into integers with Integer.parseInt.
      int day = Integer.parseInt(dayMonthYear[0]);
      int month = Integer.parseInt(dayMonthYear[1]);
      int year = Integer.parseInt(dayMonthYear[2]);
      dateOk = isLegalDate(day, month, year);
    }
    System.out.println("The date " + args[0] + (dateOk ? " is legal." : " is illegal!"));
  }
}

Non-static cannot be referenced from a static context

Most Java programmers probably sometimes encounter a compiler error of form “non-static MEMBER cannot be referenced from a static context” when trying to refer to some class member that has been defined next to the main-function. The error is caused by the fact that the other referenced member has been defined without a static modifier. As discussed later, all members defined without the static modifier are object members that can be referenced only via an existing object. Static members aka class members can be referenced always.

It should be noted that starting a Java program does not automatically create an object of the class that contains the main function. E.g. no LegalDate object exists when the preceding example program begins its execution from the main function of the LegalDate class. Thus e.g. the main function can by default refer to only static members of the LegalDate class. It is of course possible to refer to also non-static members, but this requires us to first create a LegalDate object and then make the reference via that object. An example of this will be seen later e.g. in the test main function of a linked stack implementation.

The preceding example program did not contain any loops. Below is an example illustrating Java loops. Also their syntax derives directly from C/C++. The code searches for occurrences of the string “tuni” in the command line parameters by performing characterwise comparisons. Also here we would normally probably use some readily available functions.

// If you try to run the program e.g. as "java TuniSearch use your opportunities", the program
// should find the string "tuni" in the parameter args[2] = "opportunities" and print a message:
// Found "tuni" at index 5 of opportunities.
public class TuniSearch {
  // This example contains all functionality brutely in the main function.
  public static void main(String[] args) {
    // The variable "key" holds the string to search for. Here we fix it to "tuni". The extra
    // specifier "final" corresponds to "const" in C/C++: the value of key may not be changed.
    final String key = "tuni";

    // A Java for-each loop that iterates some value sequence is similar to C++11 (and Java
    // actually had this earlier than C++). C does not have this for loop variant.
    // The variable "word" iterates over all strings in the command line parameter array "args".
    // Here we have also given the label "SEARCH_LOOP" to the outer loop. A loop label allows
    // to easily exit a nested loop by using Java's labeled break statement. C/C++ don't have
    // labeled break; they have the more general (and dangerous?) goto statement instead.
    SEARCH_LOOP:
    for(String word : args) {
      // A traditional index based for loop that iterates over all
      // potential starting positions of "key" inside "word".
      // The length of a String is given by its member function length().
      // Note how length is a function (in arrays it is a member variable).
      for(int i = 0; i <= word.length() - key.length(); i++) {
        // Now use a while loop for a change. The loop checks character by character
        // whether an occurrence of "key" is found starting at index i.
        // The String member function cal charAt(x) returns the character at index x.
        int j = 0;
        while(j < key.length() && word.charAt(i+j) == key.charAt(j)) {
          j += 1;
        }
        // Did we match all key.length() characters?
        if(j == key.length()) {
          // Now use "format" printing function for a change. It is very similar
          // to the printf function in C. One visible difference below is that
          // the format specifier %n means a new line.
          System.out.format("Found \"tuni\" at index %d of %s%n", i, word);
          // Exit the outer loop by using a labeled break statement. A normal
          // break would exit the nearest enclosing loop, but a labeled break
          // exits the loop that has a matching label.
          break SEARCH_LOOP;
        }
      }
    }
  }
}

Inferred variable type var

Java allows to define local variables with a variable type var, in which case Java will infer the actual variable type based on the type of the initialization value. This may simplify the code by allowing us to omit writing the explicit variable type. A variable defined using var is a normal typed variable: it has exactly the same properties as if we had written the type explicitly. The Java var type is more or less similar to the auto type in C++.

Because the actual type of a var variable is inferred from the initializing value, it is mandatory to specify an initializing value that has an unambiguous type. For example a null reference or a simple array initialization structure is not allowed (using var as the type of an array variable requires an explicit new operation).

Below is a short example code snippet.

// The new operation allows to infer var → Integer[].
var it = new Integer[]{1, 2, 3, 4, 5};
for(var i : it) {   // var → Integer because it is an Integer array.
  var d = 1.5 + i;  // var → double because 1.5 + i has type double.
}
// Here var → String[] because split returns a String array.
var parts = "1.1.2022".split("\\.");

The var type might be recommended only in cases where the variable has a complicated type and the actual type can be easily understood from the surrounding code. Later code examples will sometimes use var.

Java allows to define a function to take a variable number of parameters by adding three dots to the end of the last (or only) parameter’s type. The effect is that zero or more separate parameters may be given in place of such last variable parameter when calling the function. The function implementation may use the variable parameter as an array parameter: the array contains the values given in place of the variable parameter when calling the function. The array may have length 0 if no values were given for that parameter. Because a variable parameter is eventually treated as an array, Java also permits the function caller to give the “variable parameters” as an array instead of separate parameters. This makes variable function parameters quite flexible.

Below is an example function that takes two or more integers and returns their maximum value. This kind of a function might actually be useful as Java’s corresponding built-in function Math.max(int, int) accepts only exactly two parameters.

import java.util.Arrays;  // The test code needs the java.util.Arrays class.

public class Max {

  // Take the first two values as normal parameters in order to ensure that
  // the caller gives at least 2 values (and there is something to calculate).
  public static int max(int first, int second, int... rest) {
    int maxVal = (first > second) ? first : second;
    for(int val : rest) {
      if(maxVal < val) {
        maxVal = val;
      }
    }
    return maxVal;
  }

  // A test main funtion that converts command line parameters to integers
  // and computes some maxima over them.
  public static void main(String[] args) {
    int[] it = new int[args.length];
    for(int i = 0; i < args.length; i++) {
      it[i] = Integer.parseInt(args[i]);
    }

    // First a couple of test with a fixed number of parameters:
    // the calls max(it[0], it[1]), max(it[0], it[1], it[2]) and
    // max(it[0], it[1], it[2], it[3]).
    if(it.length >= 2) {
    System.out.format("Max(%d, %d) = %d%n", it[0], it[1],
                                        max(it[0], it[1]));
    }
    if(it.length >= 3) {
      System.out.format("Max(%d, %d, %d) = %d%n", it[0], it[1], it[2],
                                              max(it[0], it[1], it[2]));
    }
    if(it.length >= 4) {
      System.out.format("Max(%d, %d, %d, %d) = %d%n", it[0], it[1], it[2], it[3],
                                                  max(it[0], it[1], it[2], it[3]));
    }

    // Then a test where we compute the maximum over all command line parameters
    // (if there were at least 2) with a single function call. Since the number of parameters
    // is not known beforehand, it would not be possible to give the parameters separately.
    // But we can give the remaining parameters (after the first two) as an array.
    if(it.length >= 2) {
      // Arrays.copyOfRange(t, a, b) copies the array t interval a..b-1 into a new array.
      int[] rest = Arrays.copyOfRange(it, 2, it.length);
      // Arrays.toString gives a reasonable string representation of a primitive array.
      System.out.format("Max(%d, %d, %s) = %d%n", it[0], it[1], Arrays.toString(rest),
                                                              max(it[0], it[1], rest));
    }
  }
}

If you execute the preceding program as java Max 7 2 6 9 13 2 35 26, the expected result is:

Max(7, 2) = 7
Max(7, 2, 6) = 7
Max(7, 2, 6, 9) = 9
Max(7, 2, [6, 9, 13, 2, 35, 26]) = 35

Java file structure

Java program code consists of one or more source code files. The commonly used javac Java compiler requires that the source code files are named using the “.java” file suffix.

The preceding code examples have already given a fairly good idea of what a Java source file looks like. To be precise, the Java specification mandates that a Java source code file has the following structure:

  1. First there can be one package declaration.

    • We discuss packages later. But let us note here that their role is a bit similar to C++ namespaces and Python modules.

  2. Next can follow import statements that make external classes, interfaces or their members accessible from the source file.

    • Similar to C++ include directives and Python import statements.

  1. Next can follow one or more class / interface definitions.

    • We discuss interfaces later. At this point you can think of them as one type of classes.

    • A strong recommendation (e.g. Google Java Style Guide): define only one top level class / interface per code file.

      • This refers to a class / interface that is defined directly in the root of the file (instead of being defined within another class or interface).

The Java source file structure reflects how Java is a fairly strongly class centric programming language. All functional Java code must be written inside a class or an interface; only things permitted outside of them are package declarations and import statements. This fact that Java does not allow declaring global variables or functions is also the reason why the main function must be defined as a member function of some class and all programs must contain at least one class.

A strong recommendation, and often required by the javac compiler: a Java code file must have the same name as the (should be single) top level class or interface defined in it. E.g. if you define a top level class named TheBestClassEver, the source file should be named TheBestClassEver.java. For example theBestClassEver.java would not be ok: Java is case sensitive.

Below is an example program that illustrates the general structure of a Java code file. The main function prints out information about the current date in several different languages by employing some tools Java class library offers for handling dates and local settings (so-called “locales”).

// Note: this code should be placed in a file named WhatDayIsIt.java.

// Declare the contents of this file to belong to a package named "fi.tuni.itc.programming3".
// A file can have at most one package declaration.
package fi.tuni.itc.programming3;

// The code uses the class DateFormat from Java package java.text and the
// classes Date and Locale from the package java.util. These must be made
//  accessible by importing them with import statements.
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

// A top level class definition.
// Recommendation: one top level class / interface per file.
public class WhatDayIsIt {
  public static void main(String[] args) {
    Date now = new Date();  // Create a Date object corresponding to current time.
    // The locales used in this example: Gernamy, Japan, Korea and USA
    Locale[] locales = {Locale.GERMANY, Locale.JAPAN,
                        Locale.KOREA, Locale.US};
    System.out.println("What day is it now in different locales?");
    // Iterate over the four locales put into locales array above.
    for(Locale locale : locales) {
      // First print the locale name in US english (= Locale.US).
      System.out.print("In " + locale.getDisplayName(Locale.US) + ": ");
      // Initialize a DateFormat object df that formats a Date object according
      // to the format parameter and locale supplied to getDateInstance.
      DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, locale);
      // The call df.format(now) formats the date represented by the Date object "now".
      System.out.println(df.format(now));
    }
  }
}

Running this example program e.g. on Friday 19.11.2021 would have produced the output:

What day is it now in different locales?
In German (Germany): Freitag, 19. November 2021
In Japanese (Japan): 2021年11月19日金曜日
In Korean (South Korea): 2021년 11월 19일 금요일
In English (United States): Friday, November 19, 2021

A side note

If you wish to compile and run the preceding example on your computer. copy-paste the code into a file named WhatDayIsIt.java. At this point of the course it is advisable to remove the package dclaration in the beginning. This will allow you to compile and run the code with the usual simple commands javac WhatDayIsIt.java and java WhatDayIsIt. The extra care required by packages will be discussed later.

The Java compiler produces one separate class file with a “.class” file suffix for each different class and interface defined in the compiled code. The class files are named according to the class / interface names. E.g. compiling a HelloWorld class produces a class file HelloWorld.class regardless of how the source file was named. If a single source file contains several class or interface definitions, the compiler will produce a separate class file for each of them. Also this reflects Java’s object oriented nature: an executable program consists of a bunch of class files.

Class files contain precompiled byte code understood by a Java virtual machine (JVM). In order to be executable, at least one class of a program must contain a public static function void main(String[] args). The program is run by specifying to a JVM the name of the class from whose main function we want to start the program. Java is similar to Python in the sense that a program can in principle contain several different main functions (in different class files), whereas e.g. C/C++ programs may contain exactly one main function.

Another side note

Java class files can be executed as such on any Java virtual machine that supports the Java version used when compiling the code. If you e.g. have compiled a Hello World program into a class file HelloWorld.class, you may try to run the same class file on different systems that have a JVM installed. You could e.g. copy the class file to Windows, Linux and Mac computers and try running it on any of those with the command java HelloWorld.

A more concrete example of portability of Java programs is e.g. the Netbeans IDE, which is implemented in Java. One download options on the Netbeans website should be a zip-package that is simply extracted into some folder and run from there without any concrete “installation” steps. If the Netbeans folder would reside e.g. on a USB hard disk, the one and same Netbeans IDE instance could in principle be run on any computer that has a reasonably new JVM installed and is compatible with the USB disk.

In every Java program, there is

The function public static int max(int first, int second, int... rest) {}

Wrapper classes are used to