Lecture 28: Java Details, FYI
This course has focused on a number of key programming ideas. This section serves two purposes:
To summarize some these ideas, make sure we have words to talk about them all.
To put, in one place, some ideas that are related, but were introduced at different times.
In general, features were introduced when the solved an interesting new programming problem (e.g. interfaces for capturing the idea of method signatures shared across classes, abstract classes for the idea of method definitions shared across classes, field assignment for objects with memory, and so on). There are, of course, many many more features in Java. Knowing all of them isn’t necessary to be a good programmer, or indeed to understand object-oriented programming concepts like methods and inheritance.
However, there is a set of vocabulary and common features that you may see in the future when reading Java programs. It’s useful to be exposed to the vocabulary and keywords so that you have a point of reference when reading other programs.
You will be tested on some of this material, but it will be a very small proportion of your final exam grade. This course is about programming with objects more than it is about the specifics of Java (though it is, to some extent, about both).
14.1 Overloading Constructors and Methods
We saw that we could write constructors that took different numbers of arguments, and Java would call the correct one. For example, the ATweet constructor could be called with or without an initial number of likes. This idea – that the language selects the correct method to call based on the signature, is called overloading. It works both for constructors and for methods, though we only used it for constructors this quarter.
Overloading can also be based on the types of parameters. For example, we could define a distance method on Point with zero arguments, which computes the distance to the origin, one with a Point argument that computes the distance between two points, and another with a single double argument that treats the value as both the x and y coordinate of the other point:
class Point { double x, y; Point(double x, double y) { this.x = x; this.y = y; } double distance() { return this.distance(new Point(0, 0)); } double distance(double d) { return this.distance(new Point(d, d)); } double distance(Point other) { return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)); } } class ExamplesPoint { boolean testdist(Tester t) { t.checkInexact(new Point(3, 4).distance(), 5.0, 0.01); t.checkInexact(new Point(3, 4).distance(0), 5.0, 0.01); t.checkInexact(new Point(3, 4).distance(new Point(6, 8)), 5.0, 0.01); return true; } }
Java knows that in the first test, the zero-argument distance method should be called, in the second test, the distance method with the double argument should be called, and in the third test, the distance method with a Point argument should be called.
14.2 Overriding Methods
We saw, with abstract classes, two different kinds of interactions between subclasses and superclasses:
Inheriting a method from a superclass, as with methods like retweet that were defined once in the superclass (e.g. in ATweet) and used in all subclasses.
Providing the definition for an abstract method in a subclass, as with toHTML(), whose behavior was different between the different varieties of Tweets.
The latter is the simplest case of overriding a method, which is when a subclass defines the same method as a superclass. In the case of an abstract method, this is quite similar to implementing a method from an interface.
However, Java also allows the overriding of methods with definitions. So we could, for example, write a method that overrides the shared definition of retweet in a subclass like TextTweet if we wanted to provide a different implementation for that class. Here’s a simple example, where the tests illustrate the overriding behavior:
class Super { String m() { return "In Super"; } } class Sub1 extends Super { String m() { return "In Sub1"; } } class Sub2 extends Super { } class ExamplesOverride { boolean testOverride(Tester t) { t.checkExpect(new Sub1().m(), "In Sub1"); t.checkExpect(new Sub2().m(), "In Super"); t.checkExpect(new Super().m(), "In Super"); return true; } }
14.3 Field and Method Modifiers
You can read about this more in the official Java documentation.
It’s also worth noting that, with the exception of static, none of these would have helped us accomplish anything this quarter. Their use mostly comes from organizational benefits in large programs. It’s useful to know about them for that reason, since you’ll certainly read code in the future that uses them, but they aren’t crucial for any programming concept we’ve addressed.
14.3.1 Visibility Modifiers
public, private, and protected are access modifiers on fields and methods. They affect in which classes a particular field or method can be referred to.
private – A field declared private can only be accessed in the class that defined it. That means any uses of that field name on an object of that class will be errors when used in any other class.
Example:
class A { private int x; A() { this.x = 10; } } class B { A a; int getXFieldFromA() { return this.a.x; } // Error: "x has private access in A" } Example:
class A { private int x; A() { this.x = 10; } void addXFromOtherA(A otherA) { this.x = this.x + otherA.x; // Note that this is allowed, since the accessing code is within the class // A. The "private" is relative to code in a class, not relative to // particular objects } } class Main { public static void main(String[] args) { A myA = new A(); System.out.println(myA.x); // Error: "x has private access in A" } } This is useful in large programs because it can help us ensure that any field assignments that happen to a variable only occur within the class that uses it. Since field assignment can be tricky to test and reason about, this kind of restriction can be useful.
public – A field declared public can be accessed by any code in the program. In general, the use of public fields is discouraged if they are going to be updated. If a field will always be constant and won’t ever have its name changed, it can be useful for it to be public to avoid needing a method to access it. If the author of the class might change the name of the field in the future, they should keep it private to ensure that other programmers’ code won’t break with the name change.
default visibility – fields have a default visibility known as package private visibility. If no modifier is provided, the field is visible in all code in the same package, but not code in subclasses in different packages. Of course, understanding that definition requires understanding what a package is, which is somewhat out of scope for this course. Loosely, a package is all the Java code within a directory. So, for example, all of our Tweet implementation code was in the same package in each assignment, but the code in the tester library, or built in to Java, is in different packages.
protected – A field declared protected can be accessed within the package of the class that declares the field, or within subclasses in other packages. This is useful for patterns that use abstract classes, and is important in contrast with private. If we considered cases like the Tweets we used in programming assignments, we couldn’t make the shared fields private:
class ATweet { private String tweetId; private String content; } class TextTweet extends ATweet { String toText() { return this.tweetId + ": " + this.content; // Error: "tweetId has private access in ATweet } } However, we could use it as a protected field:
class ATweet { protected String tweetId; protected String content; } class TextTweet extends ATweet { String toText() { return this.tweetId + ": " + this.content; } } This has the benefits of restricting any uses of the field, similar to private, but allows use to construct class hierarchies to share fields across multiple classes. For our purposes this quarter, since we weren’t working across multiple packages, the default visibility was also perfectly appropriate.
14.3.2 The static modifier
The modifer static means something slightly different on fields and on methods, so we’ll tackle them one at a time.
14.3.2.1 static fields
We’ve typically thought of memory as consisting of a stack, for pending method calls, and a heap, for objects that have been created. Along with these two areas of memory, there is also a space for static fields, also called class fields. For each class in the program, there is space set aside for any static fields defined within that class. Those fields are not created per-instance or per-object, but have only a single copy in all of memory, stored in that static space.
This means, for example, that if we update a static field, any other context that looks up the same static field will see the update. This can let us share state across many objects. For example, one way to ensure that each created Tweet has a new id would have been to add a static field to ATweet:
abstract class ATweet { static int nextId; String content; String tweetId; public ATweet(String content) { ATweet.nextId += 1; this.content = content; this.tweetId = "" + ATweet.nextId; } }
The above example would have a single field, called nextId, that would be updated each time the constructor for ATweet was called. Note that we can refer to the field using the class name – we wrote ATweet.nextId. We could also use this.nextId, but I find this somewhat confusing, because it makes it hard to tell at the point of use whether a field is static or not.
Static fields are also called class fields, and non-static fields are often called instance fields.
14.3.2.2 static methods
When a method is declared static, it can be called without an accompanying value for this. In addition, since code within a static method doesn’t have a value bound for the this parameter, implicitly calling methods without an object won’t work, because there is no value to provide for this. If you find yourself in this situation, you probably need to rethink something about the structure of your program, or you forgot to make a method static.
Static methods are useful for helper functions that logically are not tied to a particular object. Many built-in methods are static, like Math.min or Files.readAllLines. These methods can be used without first constructing an object.
Like instance methods, static methods can be called by using a class’s name instead of a particular object. For example:
class Help { static void countArgs(String[] args) { System.out.println("There were " + args.length + " args"); } } class C { public static void main(String[] args) { Help.countArgs(args); } }
14.3.3 The final Modifier
The final modifer behaves differently on fields and on methods, so it’s worth tackling them separately.
14.3.3.1 final Fields
Fields declared final cannot be changed once initialized. They can be assigned within a constructor, or with a default value in the class definition, but any field assignment in another context will fail. It’s actually quite useful to make fields final when they really ought to stay the same for the whole lifetime of the object. For example, in ATweet, a number of fields should never change – the id and user of a Tweet are set when it is created and can’t change, for example. If any code tried to change these fields that were declared final, it would cause an error:
class Tweet { final String id; final User user; String content; int retweetCount; Tweet(String id, User user, String content) { this.id = id; this.user = user; this.content = content; this.retweetCount = 0; } /* Creates a new tweet with the same content as this one, but with the given user. The id of the new tweet should be this tweet's ID with "-rtN" appended, where N is the number of times this tweet has been retweeted */ Tweet retweet(User user) { // The next line is wrong – we should be making a new id for the // retweet, not changing the id of this tweet this.id = this.id + "-retweet" + this.retweetCount; // error: cannot assign a value to final variable id return new Tweet(this.id, user, this.content); } }
This error would stop us (or anyone else working on the code in the future) from making mistakes involving assigning to fields that we don’t intend to change.
14.3.3.2 final methods
A method declared with final cannot be overridden in subclassses:
class Super { final String m() { return "In Super"; } } class Sub1 extends Super { String m() { // error: m() in Sub1 cannot override m() in Super return "In Sub1"; } } class Sub2 extends Super { } class ExamplesOverride { boolean testOverride(Tester t) { t.checkExpect(new Sub1().m(), "In Sub1"); t.checkExpect(new Sub2().m(), "In Super"); t.checkExpect(new Super().m(), "In Super"); return true; } }
This could be useful for avoiding mistakes where a subclass replaces an important, shared implementation provided by a superclass. For example, it was important that in the quote method in the tweets examples, the retweetCount was incremented and that QuoteTweets were constructed in a particular way. It could be useful to make that quote method final to avoid any subclass handling quote differently.
14.4 int vs Integer
In class when we introduced generics, we used the Integer type in lieu of the type int. It turns out that for all the primitive types we’ve used so far—int, char, double, long—there is a corresponding class. That is, there exist built-in classes Integer, Character, Double, and Long.
These classes have useful static helper methods—like Double.parseDouble—and also can be instantiated. For example, we could write new Double(1.2) and a new object of class Double would be created. For example, consider this code:
class ExamplesDouble { boolean testD(Tester t) { double d = 12; Double d2 = new Double(12); } }
When the testD method runs, it produces the following:
That is, we can think of these objects as containing a single (private) field that will never change, and contains the numeric value.
One main use of these objects is actually for generics. Java doesn’t let us instantiate a generic type with a primitive. For example, its an error to write:
interface AnyList<T> { } class Link<T> implements AnyList<T> { } class Empty<T> implements AnyList<T> { } class ExamplesList { AnyList<int> l = new Link<int>(); // error: unexpected type }
But we can write:
interface AnyList<T> { } class Link<T> implements AnyList<T> { } class Empty<T> implements AnyList<T> { } class ExamplesList { AnyList<Integer> l = new Link<Integer>(); }
There’s no particularly good motivation for this from the programmer’s perspective. It’s simply a restriction in the way Java works.
In many cases, these objects behave just the same as their primitive value counterparts – printing, arithmetic, and so on work mostly as you’d hope. The converse isn’t true, because primitive values like int and double do not have methods defined on them, so it’s an error to write (5).equals(10). Since many built-in operations rely on or return the primitive types, and the object versions are needed for working with generics, we unfortunately have to live with the reality that both are used.
14.5 Definition: Primitive vs. Reference
A piece of vocabulary that is often used to distinguish object types from types like int and double is primitive vs. reference type. This goes for all types – any programmer-created class, like ATweet, along with built-in ones, like Integer or String, and array types, are all collectively categorized as reference types. Non-object values are called primitive types.
The Java Documentation has a full accounting of the different primitive types, which go a little bit beyond what we used, but they are straightforward extensions:
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html