A C-Hacker's Briefing on Java by Robin Popplestone The Good News: The syntax of statements and expressions is close to C. The Bad News: But you've got to learn about classes, 'cos all code is wrapped up as a -method- in a class. CONTENTS - (Use g to access required sections) 1 Primitive Types 2 Reference Types 2.1 Accessing member variables 2.2 Casting: does making a new class-instance need it? 2.3 Class descriptors 2.4 Methods 2.5 The Class Hierarchy 2.6 Constructing Java Objects 2.7 Access specifiers. 2.8 Static member-variables and methods 3 Help! How do I get Java to do anything 4 Interfaces ----------------------------------------------------------------------- 1 Primitive Types ----------------------------------------------------------------------- Java has much the same set of primitive types as the arithmetic types of C. Note however that the char type in Java is 16-bits, to support _______unicode characters, and that Java has a separate boolean type. This latter is good news, because almost all those bugs (n=0) ? 1 : n*fact(n-1) can't happen. The only members of the boolean type are true and false. Generally, most of the code you'd write using these primitive types will be the same as in C, but Java is a bit more picky about conversions between arithmetic types (and of course if a boolean is needed, you must supply one. For example while (1) {doit();} will earn you a black mark with the Java compiler). ----------------------------------------------------------------------- 2 Reference Types ----------------------------------------------------------------------- Java, you've heard, has no pointers. Actually, it's perhaps better to think that Java provides, for non-primitive types, nothing but pointers. But the system only ever lets you get your hands on a pointer to an object that it knows all about. Let's consider the following C, which defines a structure to hold some basic information about a person: struct person {char* name; int age; char* sex;}; typedef struct person * Person; This may be approximated in Java as: class Person { String name; int age; String sex; } So, if we say in C Person p; we ask the C compiler to allocate a variable p big enough to hold a pointer to a Person structure (or any other for that matter). And we can say exactly the same thing in Java. 2.1 Accessing member variables ------------------------------- Now once we have a handle on a person-structure, in C we'd evaluate the expression: p -> age to obtain the age of that person, as being short-hand for (*p).age. Now in Java the dereferencing is implicit, so we say just p.age to obtain the same value. 2.2 Casting: does making a new class-instance need it? ------------------------------------------------------- In Java the memory for all class-instances that you create is allocated off the heap.And the old comfortable malloc way of doing things is not allowed, because it requires an arbitrary cast. We can tell cc the unlikely story: Person p1 = (Person) 245; /* BAD */ but woe betide you if you try telling this to the Java compiler, javac. So, let's consider how we might acquire a new Person object. In C we could write something like Person new_Person(char* name, int age, char* sex) /* 1 */ {Person p; /* 2 */ p = (Person) malloc(sizeof (struct person)); /* 3 */ p->name = name; p->age = age; p->sex = sex; /* 4 */ return p;} /* 5 */ 2.3 Class descriptors ---------------------- However of course the cast in line 3 above is used in exactly the same way as the cast in line BAD. So, let's see how we go about creating objects, and computing with them in Java. The basic idea is that at run-time Java always knows what class an object belongs to, but, since it would be too restrictive to require that that class is known exactly at compile time, objects always carry a _____class-__________descriptor along with them so that certain run-time decisions (e.g. may I cast this instance to that instance) can be made in a principled way. If we were disciplined about following a set of rules, we could do the same thing in C. We might have something like: struct class_descriptor {char* name; /* the name of the class */ char** fields; /* a description of its fields*/ }; typedef struct class_descriptor * ClassDescriptor; struct person {ClassDescriptor class; char* name; int age; char* sex;}; typedef struct person * Person; Suppose also we followed the rule that for each class of structures we created a unique _____class __________descriptor. So there would be a class-descriptor for persons, and one for let us say, corporations. Then we'd make some global declarations: ClassDescriptor person_descriptor; ClassDescriptor corporation_descriptor; void initialise_system() { ...code to fill in fields of class descriptors... } Person new_Person(char* name, int age, char* sex) /* 1 */ {Person p; /* 2 */ p = (Person) malloc(sizeof (struct person)); /* 3 */ p->name = name; p->age = age; p->sex = sex; /* 4 */ p->class = person_descriptor; return p;} /* 5 */ 2.4 Methods ------------ If we did this in a disciplined way so that every object we created had its proper class-descriptor filled in, then we could always tell at run-time what kind of data-object a pointer was pointing to. Moreover, we could put some useful code in the class-descriptor: struct class_descriptor {char* name; /* the name of the class */ char** fields; /* a description of its fields*/ void (*print_method)(void*); }; Here the print_method field contains a pointer to a function to print a member of a given class, whatever it might be. And we could write a function that would print any object thus: struct object{ ClassDescriptor class;} typedef struct object* Object; void print_object(Object obj) {obj->class->print_method(obj);} We could draw a diagram of a class, with its instances and class-descriptors. CLASS DESCRIPTOR -------------------------------- ---------------- |class "person" | | class --+-----------------> |... other info | |--------------| | |fields = {"name","age","sex"} | | name="Robin" | | |print = void print(this){...} | | age = 60 | | -------------------------------- | sex = "M" | | ---------------- | | | ---------------- | | class -+------------- |--------------| | name="Jane" | | age ="59" | | sex ="F" | ---------------- Well, all this building of descriptors takes place automatically in Java. The basic declaration: class Person { String name; int age; String sex; } will build the class-descriptor. If we want to add a minimal printing capability we write it thus: class Person { String name; int age; String sex; void print() {System.out.println( "name " + name + " age " + age + " sex " + sex);} } In this case, the Java system knows that name,age and sex are to be put in the individual object records, but that the print-method is common to all objects of the class, so it should go in the class-descriptor. Indeed, the only way you can have code in Java is as a method. If p is a person, then the ___________method-call p.print() will print out the object. Note that the addition sign in the definition of the print method denotes the concatenation of strings, and that members of all Java data-types can be converted to type string, and will be when "added" to a string. 2.5 The Class Hierarchy ------------------------ In Java classes form a _________hierarchy. When we say that one class C1 extends a class C2 if every instance of C2 has all the fields and methods pertaining to C1. We could do this in C. Suppose we wanted to represent a new class student, which had an additional field: struct student {ClassDescriptor class; char* name; int age; char* sex; char** courses; }; typedef struct student* Student; then we could treat a given student s as being a person just by a cast p = (Person) s If we'd made sure that the Student structure began exactly in the same way as a person structure, and that an appropriate class-descriptor was present, then all our code for operating on members of the Person class would work for members of the Student class. Therefore it's fair to regard the above cast as safe. However the opposite cast, from an arbitrary Person pointer to a Student pointer would be unsafe, since we might have a Person structure without the extra slots that made it a Student. Java automates the whole process. If we say: class Student extends Person {String courses[]; } we obtain a new Student class which, by default ________inherits all of the fields of the Person class, and all its methods. It has a new field, courses, which is an array of strings. Arrays are a reference type distinct from objects (but they have their own descriptors, so you can always tell at run time how long an array is, for example). We can ________override a method class Student extends Person {String courses[]; void print() {System.out.println( "name " + name + " age " + age + " sex " + sex + " courses " + courses);} } In this case, the class-descriptor for the Student class contains the code for this method. Otherwise the code for the Person method would be used. A cast of an object in Java works just as a cast of a pointer works in C - essentially it's an identity function, ______except that not all casts are legal. Because a user-defined class always extends exactly one other class, the class hierarchy is a tree. It's always legal to cast up the tree. So we can always cast a Student object to be a Person. But we can't cast sideways. So trying to cast a Student to be a Professor will be a compile-time error. Casting down the hierarchy is not a compile-time error. So we can cast a person to be a student. However such a cast is always checked at run-time, and may give a run-time error. So if p was a professor, the code s = (Student)(Person)p; would not (necessarily) give a compile-time error, but would certainly give a run-time error. Object | | | ------------------- | | | | | | Person Institution | ----------- Student Professor 2.6 Constructing Java Objects ------------------------------ So far we havn't said how we actually make an object in Java. There's only one way, we use the new construct. If we evaluate the statement s = new Student(); we make the variable s have as value a Student object freshly created ___off ___the ____heap. The lifetime of this object is arbitrary. Essentially the Java system will reclaim the store it occupies using the _______garbage _________collector, perhaps familiar to you from Scheme in CMPSCI287. However, unlike Scheme and other functional, or quasi-functional, languages, the process of creating a new object in Java has two important phases. (1) Allocating the store (2) Calling the appropriate "constructor" method. A constructor is a method whose name is the same as that of the class containing it, and which has no return type. A constructor ____does ___not ________allocate ______memory - this is done by new, which is a syntactic form. A constructor is intented to allow the user to initialise the newly allocated object as she thinks fit. By default, a nullary constructor is created for a class. So in the above code, Student() is a call to this default nullary constructor. If you write a constructor method, it will typically initialise one or more of the fields of the object being created. class Person { String name; int age; String sex; Person(String n, int a, String s) {name = n; age = a; sex = s;} } We could also write this as: class Person { String name; int age; String sex; Person(String n, int a, String s) {this.name = n; this.age = a; this.sex = s;} } Here this is a keyword used in methods to refer to the object-instance for which the method is called. In general, a Java class can have several methods (including constructors) of the same name, provided that Java can determine at compile-time (by a process of argument-type-matching) which method to use. 2.7 Access specifiers. ----------------------- We use ______access __________specifiers to control how much of a class, or an instance of a class, is accessible from outside the class. These specifiers are given in the table. ------------------------------------------------------- | Modifier | Class | Subclass | Package |World | |---------------+-------+-----------+---------+-------| | private | Yes | | | | | protected | Yes | Yes | Yes | | | public | Yes | Yes | Yes | Yes | | | Yes | | Yes | | ------------------------------------------------------- Note that if no modifier is used (the last case in the table above) accessibility is restricted to the class itself and the package it's in. 2.8 Static member-variables and methods ---------------------------------------- So far we've assumed that, apart from methods, every entity that you declare in a class definition is held in a slot in a record that is the implementation of an instance of that class. But it can be convenient to have some other quantities whose value is held in a slot in the class-descriptor. For example, you might want to keep a count of how many instances of a class you've created. If the static keyword is used in a (non-method) declaration within a class, then the entity declared will be allocated in the class-descriptor record, so there will be only one for the whole class. class Person { String name; int age; String sex; static int count; Person(String n, int a, String s) {this.name = n; this.age = a; this.sex = s; count=count+1; } } Methods can also be declared static, but the sense is a bit different. Methods are ______always held as part of the class-descriptor, and are never held in class-instances. However, if you declare a method to be static, it behaves rather like an ordinary C function. It doesn't have a class-instance passed to it as a hidden parameter, and consequently it can be referred to by the class it's defined in. For example, System.out.println(new Person("fred",23,"m")); is a call to a static method of a standard Java class. ----------------------------------------------------------------------- 3 Help! How do I get Java to do anything ----------------------------------------------------------------------- Well, you have to write a file, with the extension ".java", which contains a class definition with a static main method of the right kind. In file test.java class TestPerson {public static void main(String[] args) { System.out.println(new Person("fred",23,"m")); } } class Person { String name; int age; String sex; public String toString() {return "name " + name + " age " + age + " sex " + sex;} Person(String n, int a, String s) {this.name = n; this.age = a; this.sex = s;} } You then compile this code. Try javac test.java. This will create two _____class files, whose names are TestPerson.class and Person.class. You can then give the Unix command java TestPerson which will examine the TestPerson class file, and if it contains an appropriate main method, that will be executed. ----------------------------------------------------------------------- 4 Interfaces ----------------------------------------------------------------------- A user-defined Java class has exactly one super-class which it extends. However it can _________implement any number of interfaces. Interfaces can be thought of as being Java's version of .h files in C, but they're much better, being well structured.