Java is an object-oriented programming language, which views the world as a collection of objects that have both properties and behavior. Java's version of object-orientedness is pretty straightforward, and it's the basis for almost everything in the language. Because it's so essential to Java, I'll explain a bit about what's under the covers to help anyone new to the language.
In general, all Cartesian geometric objects, like circles, squares, triangles, lines, and points, have basic properties, like location and extension. Objects with zero extension, like points, usually don't have anything more than that. Objects like lines have more—e.g., the start and endpoint of a line segment or two points along a line (if it's a "true line"). Objects like squares or triangles have still more—the corner points, for example—whereas circles may have a center and radius.
We can see there is a simple hierarchy at work here: The general geometric object can be extended into specific geometric objects, like points, lines, squares, etc. Each specific geometric object inherits the basic geometric properties of location and extension and adds its own properties.
This is an example of single inheritance. Java's original object-oriented model allowed only single inheritance, where objects cannot belong to more than one inheritance hierarchy. This design decision comes out of the kinds of ambiguities programmers found themselves facing in complex multiple-inheritance scenarios, typically in cases where "interesting design decisions" led to several possible implementations of the function foo() as defined (and re-defined) in the hierarchy.
Since Java 8, there has been a limited multiple inheritance structure in place that requires specific actions on behalf of the programmer to ensure there are no ambiguities.
Strong and static typing
Java is strongly and statically typed. What does this mean?
A statically typed language is one where the type of a variable is known at compile time. In this situation, it is not possible to assign a value of type B to a variable whose declared type is A, unless there is a conversion mechanism to turn a value of type B into a value of type A. An example of this type of conversion is turning an integer value, like 1, 2, or 42, into a floating-point value, like 1.0, 2.0, or 42.0.
A strongly typed language is one where very few (or perhaps no) type conversions are applied automatically. For example, whereas a strongly typed language might permit automatic conversion of integer to real, it will never permit automatic conversion of real to integer since that conversion requires either rounding or truncation in the general case.
Primitive types, classes, and objects
Java provides a number of primitive types: byte (an eight-bit signed integer); short (a 16-bit signed integer); int (a 32-bit signed integer); long (a 64-bit signed integer); float (a single precision 32-bit IEEE floating-point number); double (a double precision 64-bit IEEE floating-point number); boolean (true or false); and char (a 16-bit Unicode character).
Beyond those primitive types, Java allows the programmer to create new types using class declarations. Class declarations are used to define object templates, including their properties and behavior. Once a class is declared, instances of that class can generally be created using the new keyword. These instances correspond directly to the "objects" we have been discussing. Java comes with a library of useful class definitions, including some simple basic classes such as String, which is used to hold a sequence of characters like "Hello, world."
Let's define a simple message class that contains the name of the sender as well as the message text:
There are several important things to note in this class declaration:
- The class is (by convention) always declared with a leading capital letter.
- The Message class contains two properties (or fields):
– a String field called sender
– a String field called text
Properties or fields are (by convention) always declared with a leading lower-case letter.
- There is some kind of thing that starts with public Message.
– It is a method (methods define the behavior of an object).
– It is used to construct instances of the class Message.
– Constructor methods always take the same name as the class and are understood to return an instance of the class once it's constructed.
– Other methods are (by convention) always declared with a leading lower-case letter.
– This constructor is "public," meaning any caller can access it.
- As part of the construction process, some lines start with this.
– this refers to the present instance of the class.
– Thus this.sender refers to the sender property of the object.
– Whereas plain sender refers to the parameter of the Message constructor method.
– Therefore, these two lines are copying the values provided in the call to the constructor into the fields of the object itself.
So we have the Method class definition. How do we use it? The following code excerpt shows one possible way:
Message message = new Message("system", "I/O error");
Here we see:
- The declaration of the variable message of type Message
- The creation of a new instance of the Message class with sender set to "system" and text set to "I/O error"
- The assignment of that new instance of Message to the variable message
- If later in the code, the variable message is assigned a different value (another instance of Message) and no other variable was created that referred to this instance of Message, then this instance is no longer used by anything and can be garbage-collected.
The key thing happening here is that we are creating an object of type Message and keeping a reference to that object in the variable message.
We can now use that message; for instance, we can print the values in the sender and text properties, like this:
This is a very simple and unsophisticated class definition. We can modify this class definition in a number of ways:
- We can make the implementation details of properties invisible to callers by using the keyword private in front of the declarations, allowing us to change the implementation without affecting callers.
- If we choose to conceal properties in the class, we would then typically define procedures for getting and setting those properties; by convention in Java these would be defined as:
– public String getSender()
– public String getText()
– public void setSender(String sender)
– public void setText(String text)
- In some cases, we may wish to have "read-only" properties; in those cases, we would not define setters for such properties.
- We can make the constructor of the class invisible to callers by using the private keyword instead of public. We might wish to do this when we have another class whose responsibility is creating and managing a pool of messages (possibly executing in another process or even on another system).
Now, suppose we want a kind of message that records when it was generated. We could declare it this like:
Here we see some new things:
- TimedMessage is extending the Message class—that is, TimedMessage is inheriting properties and behavior from Message.
- The constructor calls the constructor in its parent, or superclass, with the values of sender and text passed in, as super(sender, text), in order to make sure its inherited properties are properly initialized.
- TimedMessage adds a new property, creationTime, and the constructor sets it to be the current system time in milliseconds.
- Time in milliseconds in Java is kept as a long (64-bit) value (0 is 1 January, 1970 00:00:00 UTC).
- As a bit of an aside, the name creationTime suggests it should be a read-only property, which also suggests that the other properties be read-only; that is, TimedMessage instances should probably not be reused nor have their properties altered.
The Object class
"The Object class" sounds like a kind of contradiction in terms, doesn't it? But notice that the first class we defined, Message, did not appear to extend anything—but it actually did. All classes that don't specifically extend another class have the class Object as their immediate and only parent; therefore, all classes have the Object class as their ultimate superclass.
You can learn more about the Object class in Java's docs. Let's (briefly) review some interesting details:
- Object has the constructor Object(), that is, with no parameters.
- Object provides some useful methods to all of its subclasses, including:
– clone(), which creates and returns a copy of the instance at hand
– equals(Object anotherObject), which determines whether anotherObject is equal to the instance of Object at hand
– finalize(), which is used to garbage-collect the instance at hand when it is no longer used (see above)
– getClass(), which returns the class used to declare the instance at hand
— The value returned by this is an instance of the Class class, which permits learning about the declaring class at runtime—a process referred to introspection.
- hashCode() is an integer value that gives a nearly unique value for the instance at hand.
– If the hash codes of two distinct instances are equal, then they may be equal; a detailed comparison of the properties (and perhaps methods) is necessary to determine complete equality;
– If the hash codes are not equal, then the instances are also not equal.
– Therefore, hash codes can speed up equality tests.
– A hash code can also be used to create a HashMap (a map is an associative array or dictionary that uses the hash code to speed up lookups) and a HashSet (a set is a collection of objects; the programmer can test whether an instance is in a set or not; hash codes are used to speed up the test).
- notify(), notifyAll(), wait(), wait(long timeout), and wait(long timeout, int nanos) communicate between collaborating instances executing on separate threads.
- toString() produces a printable version of the instance.
We've touched on some important aspects of object-oriented programming, Java-style. There are six important, related topics that will be covered in future articles:
- Namespaces and packages
- Overriding methods in subclasses—for instance, the String class has its own specific hashCode() method that recognizes its meaning as an array of characters; this is accomplished by overriding the hashCode() method inherited from Object
- Interfaces, which permit describing behavior that must be provided by a class that implements the interface; instances of classes that implement a given interface can be referred to by that interface when the only thing of interest is that specific behavior
- Arrays of primitives or classes and collections of classes (such as lists, maps, and sets)
- Overloading of methods—where several methods with the same name and similar behavior have different parameters
- Using libraries that don't come with the Java distribution
Is there anything you would like to read next? Let us know in the comments and stay tuned!