Quasi-objects #
Enumerated types #
Most imperative languages allow the creation of so-called enumerated types.
An enumerated type is a datatype for a finite set or enumerated values. For instance, if our game only allows “blue”, “green” and “red” as unit colors, then we may create a dedicated type that only allows these three values.
In Java #
An enumerated type in Java is a set of constants. These constants are effectively static, meaning that they depends on the class (or interface) where the enumerated type is declared (similarly to a static attribute). The name of an enumerated type can generally be used like a regular reference type. For instance
public class Unit {
enum UnitColor {BLUE, GREEN, RED}
int health;
UnitColor color;
public Unit(int health, UnitColor color){
this.health = health;
this.color = color;
}
...
}
Note. In the above example, the enumerated type
UnitColor
could equivalently be represented with an integer. For instance,0
for blue,1
for green and2
for red. However, with such an encoding, a conditional statement (e.g.if
orswitch
) that checks the color of a unit would also need to handle the case of values< 0
or> 2
. So (besides readability), a benefit of the enumerated type in this case is that it restricts possible inputs to valid ones.
Hint. Enumerated types in Java are significantly more expressive than their counterparts in some other languages. Notably, a Java
enum
can have its own constructor and methods.
Records #
A record is an object whose attributes cannot be modified.
in Java #
Records were introduced in Java 14 (2020). They are a convenient way to avoid boilerplate code. Records provide a concise syntax for “lightweight” classes that are only meant to hold data.
For instance, here is the full implementation of a record City
:
public record City(String name, int zipCode) {}
This record has an implicit constructor, and implements equals
, hashcode
and toString
in the expected way.
For instance:
City florence = new City("Florence", 50100);
City florenceAgain = new City("Florence", 50100);
// Outputs true.
System.out.println(florence.equals(florenceAgain));
The attributes of a record cannot be modified. For instance, the following program does not compile:
City florence = new City("Florence", 50100);
florence.zipCode = 50121;
A record is often declared inside a class or interface. For instance, in our game:
public class Board {
public record TileCoordinates(int X, int Y){};
...
}
Records are convenient for methods that need to return more than one value. For instance:
TileCoordinates getOngoingMove();
Strings #
Interning #
Many modern languages (such as Java, C#, Python, Ruby, JavaScript, Go, etc.) use an optimization technique called interning in order to manage strings (or sometimes integers, etc.) in memory. This consists in storing only one copy of each distinct string created during the execution of a program. These strings are stored in a shared pool, and each of them is an immutable object (meaning that it cannot be modified).
Some benefits are faster string comparisons, lower memory footprint, etc.
in Java #
In Java, a string is an object.
Like for regular objects, comparing two variables of type String
with ==
compares their references:
String myString = new String("foo");
String sameString = new String("foo");
// Outputs false, because the two variables hold different references.
System.out.println(myString == sameString);
A string can be explicitly interned (i.e. added to the shared pool) with the (instance) method intern
.
This method:
- tries to add the string to the pool, and then
- returns a reference to the (only) version of the string contained in the pool.
For instance, consider the following program:
// Creates a string "foo",
String myString = new String("foo")
// adds it to the pool,
myString = myString.intern();
// Creates another string "foo",
String sameString = new String("foo");
// Outputs false.
System.out.println(myString == sameString);
// Tries to add "foo" to the pool once again.
// Because the pool already contains a version of "foo", returns a reference to it.
sameString = sameString.intern();
// Outputs true, because the two variables now hold the same reference.
System.out.println(myString == sameString);
A string that is initialized without constructor (i.e. directly with " “) is interned. For instance
String myString = "foo";
String sameString = "foo";
// Outputs true, because of (implicit) interning
System.out.println(myString == sameString);
In order to support interning, string in Java are immutable.
In particular, the instance methods of the class String
do not modify the current string, even if the names of some of these methods (substring
, replace
, etc.) suggest that they do.
Instead, these methods (may) return (a reference to) a different object.
For instance:
String foo = new String("foo");
String copy = foo;
// Outputs true, because the two variables hold the same object reference.
System.out.println(copy == foo);
foo = foo.replace('o','O');
// Outputs false, because the method 'replace' returns a reference
// to another object.
System.out.println(copy == foo);
The class String
overrides the method equals so that it implements string comparison in the expected way.
For instance:
String myString = new String("foo");
String sameString = new String("foo");
// Outputs false.
System.out.println(myString == sameString);
// Outputs true.
System.out.println(myString.equals(sameString));
Hint. By default, you can always use
equals
to compare the values of two strings. This is less error-prone than==
(albeit less efficient if the two strings differ).
Java boxed types #
Java has 8 primitive types: byte
, short
, int
, long
, float
, double
, boolean
and char
.
For each primitive type, Java has a so-called boxed or (wrapper) type:
Byte
, Short
, Integer
, Long
, Float
, Double
, Boolean
and Character
.
These types can be used in Java collections (Set
, List
, etc.), whereas primitive types cannot.
Some boxed types also offer additional functionalities, via instance methods (e.g. an Integer
can represent a positive numbers up to $2^{32} - 1$.
Constructors for boxed types are deprecated. Instead, instances can be created via so-called autoboxing, for instance:
Integer myInteger = 2;
Character myCharacter = 'a';
Several Java operators (like +
, ==
, <=
, etc.) are also overloaded so that they behave with boxed types as they would with primitive types.
For instance:
Integer i = 1;
i += 1;
Integer j = 2;
// Outputs true
System.out.println(i == j);
Like strings, instances of boxed types are immutable. This can affect performance. For instance:
for (Integer i = 0; i < 1000000 ; i++){
...
}
may create up to 1 million objects in memory. This is one of the reasons why it is usually recommended to use primitive types whenever possible (see for instance Effective Java, Item 61). Some boxed types are (partially) interned, analogously to strings.