Core Java Tutorial
Java Architecture
1. Compilation and interpretation in
Java
Java combines both the approaches of compilation and
interpretation. First, java compiler compiles the source code into bytecode. At
the run time, Java Virtual Machine (JVM) interprets this bytecode and generates
machine code which will be directly executed by the machine in which java
program runs. So java is both compiled and interpreted language.
Figure 1: Java Architecture
2. Java Virtual Machine (JVM)
JVM is a component which provides an environment for running
Java programs. JVM interprets the bytecode into machine code which will be
executed the machine in which the Java program runs.
3. Why Java is Platform Independent?
Platform independence is one of the main advantages of Java.
In another words, java is portable because the same java program can be
executed in multiple platforms without making any changes in the source code.
You just need to write the java code for one platform and the same program will
run in any platforms. But how does Java make this possible?
As discussed early, first the Java code is compiled by the
Java compiler and generates the bytecode. This bytecode will be stored in
class files. Java Virtual Machine (JVM) is unique for each
platform. Though JVM is unique for each platform, all interpret the
same bytecode and convert it into machine code required for its own platform
and this machine code will be directly executed by the machine in which java
program runs. This makes Java platform independent and portable.
Let’s make it more clear with the help of the following
diagram. Here the same compiled Java bytecode is interpreted by two
different JVMS to make it run in Windows and Linux platforms.
4. Java Runtime Environment (JRE)
and Java Architecture in Detail
Java Runtime Environment contains JVM, class libraries and
other supporting components.
As you know the Java source code is compiled into bytecode
by Java compiler. This bytecode will be stored in class files. During runtime,
this bytecode will be loaded, verified and JVM interprets the
bytecode into machine code which will be executed in the machine in which the
Java program runs.
A Java Runtime Environment performs the following main tasks
respectively.
1. Loads the class
This is done by the class loader
2. Verifies the bytecode
This is done by bytecode verifier.
3. Interprets the bytecode
This is done by the JVM
These tasks are described in detail in the subsequent
sessions.A detailed Java architecture can be drawn as given below.
Figure 3: Java Architecture in Detail
4.1. Class loader
Class loader loads all the class files required to execute
the program. Class loader makes the program secure by separating the namespace
for the classes obtained through the network from the classes available
locally. Once the bytecode is loaded successfully, then next step is bytecode
verification by bytecode verifier.
4.2. Byte code verifier
The bytecode verifier verifies the byte code to see if any
security problems are there in the code. It checks the byte code and ensures
the followings.
1. The code follows JVM specifications.
2. There is no unauthorized access to memory.
3. The code does not cause any stack overflows.
4. There are no illegal data conversions in the code such as
float to object references.
Once this code is verified and proven that there is no
security issues with the code, JVM will convert the byte code into machine code
which will be directly executed by the machine in which the Java program runs.
4.3. Just in Time Compiler
You might have noticed the component “Just in Time” (JIT)
compiler in Figure 3. This is a component which helps the program execution to
happen faster. How? Let’s see in detail.
As we discussed earlier when the Java program is executed,
the byte code is interpreted by JVM. But this interpretation is a slower
process. To overcome this difficulty, JRE include the component JIT compiler.
JIT makes the execution faster.
If the JIT Compiler library exists, when a particular
bytecode is executed first time, JIT complier compiles it into native machine
code which can be directly executed by the machine in which the Java program
runs. Once the byte code is recompiled by JIT compiler, the execution time
needed will be much lesser. This compilation happens when the byte code is about
to be executed and hence the name “Just in Time”.
Once the bytecode is compiled into that particular machine
code, it is cached by the JIT compiler and will be reused for the future needs.
Hence the main performance improvement by using JIT compiler can be seen when
the same code is executed again and again because JIT make use of the machine
code which is cached and stored.
5. Why Java is Secure?
As you have noticed in the prior session “Java Runtime
Environment (JRE) and Java Architecture in Detail”, the byte code is inspected
carefully before execution by Java Runtime Environment (JRE). This is
mainly done by the “Class loader” and “Byte code verifier”. Hence a high level
of security is achieved.
6. Garbage Collection
Garbage collection is a process by which Java achieves
better memory management. As you know, in object oriented programming, objects
communicate to each other by passing messages. (If you are not clear about the
concepts of objects, please read the prior chapter before continuing in this
session).
Whenever an object is created, there will be some memory allocated for this object. This memory will remain as allocated until there are some references to this object. When there is no reference to this object, Java will assume that this object is not used anymore. When garbage collection process happens, these objects will be destroyed and memory will be reclaimed.
Garbage collection happens automatically. There is no way that you can force garbage collection to happen. There are two methods “System.gc()” and “Runtime.gc()” through which you can make request for garbage collation. But calling these methods also will not force garbage collection to happen and you cannot make sure when this garbage collection will happen.
OOPS
4
pillars of OOPS
Abstraction
Ploymorphism
Inheritance
Encapsulation
Abstraction :
Refers to the act of representing the essential features without including
background details or explanations
What is
Abstraction?
Abstraction is process of hiding the implementation details and showing only the functionality.
Abstraction in java is achieved by using interface and abstract class. Interface give 100% abstraction and abstract class give 0-100% abstraction.
What is Abstract class in Java?
A class that is declared as abstract is known as abstract class.
Syntax:
abstract class <class-name>{}
An abstract class is something which is incomplete and you cannot create instance of abstract class.
If you want to use it you need to make it complete or concrete by extending it.
Abstraction is process of hiding the implementation details and showing only the functionality.
Abstraction in java is achieved by using interface and abstract class. Interface give 100% abstraction and abstract class give 0-100% abstraction.
What is Abstract class in Java?
A class that is declared as abstract is known as abstract class.
Syntax:
abstract class <class-name>{}
An abstract class is something which is incomplete and you cannot create instance of abstract class.
If you want to use it you need to make it complete or concrete by extending it.
A
class is called concrete if it does not contain any abstract method and
implements all abstract method inherited from abstract class or interface it
has implemented or extended.
What is Abstract method in Java?
A method that is declare as abstract and does not have implementation is known as abstract method.
If you define abstract method than class must be abstract.
Syntax:
abstract return_type method_name ();
An abstract method in Java doesn't have body, it’s just a declaration. In order to use abstract method you need to override that method in Subclass.
Example 1 :( Without abstract method)
class Employee extends Person {
private String
empCode;
public String
getEmpCode() {
return
empCode;
}
public void
setEmpCode(String empCode) {
this.empCode =
empCode;
}
}
abstract class Person
{
private String
name;
public String
getName() {
return name;
}
public void
setName(String name) {
this.name =
name;
}
}
public class Main{
public
static void main(String args[]){
//INSTIATING
AN ABSTRACT CLASS GIVES COMPILE TIME ERROR
//Person p
= new Person() ;
//THIS
REFERENCE VARIABLE CAN ACESS ONLY THOSE METHOD WHICH ARE OVERRIDDEN
Person person
= new Employee();
person.setName("Kiran Putcha");
System.out.println(person.getName());
}
}
Example 2: (with abstract method)
public class Main{
public
static void main(String args[]){
TwoWheeler
test = new Honda();
test.run();
}
}
abstract class
TwoWheeler {
public abstract
void run();
}
class Honda extends TwoWheeler{
public
void run(){
System.out.println("Running..");
}
}
When do you use abstraction?
When you know something needs to be there but not sure how exactly it should look like.
Advantages of Abstraction
By using abstraction, we can separate the things that can be grouped to another type.
Frequently changing properties and methods can be grouped to a separate type so that the main type need not undergo changes. This adds strength to the OOAD principle -"Code should be open for Extension but closed for Modification".
Simplifies the representation of the domain models.
Summary:
- Use abstraction if you know something needs to be in class but implementation of that varies.
- In Java you cannot create instance of abstract class , its compiler error.
- abstract is a keyword in java.
- A class automatically becomes abstract class when any of its method declared as abstract.
- abstract method doesn't have method body.
- Variable cannot be made abstract, its only behavior or methods which would be abstract.
- If a class extends an abstract class or interface it has to provide implementation to all its abstract method to be a concrete class. Alternatively this class can also be abstract.
Polymorphism:
The
word ‘polymorphism’ literally means ‘a state of having many shapes’ or ‘the
capacity to take on different forms’. When applied to object oriented
programming languages like Java, it describes a language’s ability to process
objects of various types and classes through a single, uniform interface.
Polymorphism
in Java has two types: Compile time polymorphism (static binding) and Runtime
polymorphism (dynamic binding). Method overloading is an example of static
polymorphism, while method overriding is an example of dynamic polymorphism.
An
important example of polymorphism is how a parent class refers to a child class
object. In fact, any object that satisfies more than one IS-A
relationship is polymorphic in nature.
For
instance, let’s consider a class
Animal
and let Cat
be a subclass of Animal
. So, any cat IS animal. Here, Cat
satisfies the IS-A
relationship for its own type as well as its super class Animal
.
Note: It’s also legal to
say every object in Java is polymorphic in nature, as each one passes an IS-A
test for itself and also for
Object
class.
Static Polymorphism:
In
Java, static polymorphism is achieved through method overloading. Method
overloading means there are several methods present in a class having the same
name but different types/order/number of parameters.
At
compile time, Java knows which method to invoke by checking the method
signatures. So, this is calledcompile time polymorphism or static binding. The concept will be
clear from the following example:
class DemoOverload{
public int add(int x, int y){ //method 1
return x+y;
}
public int add(int x, int y, int z){ //method 2
return x+y+z;
}
public int add(double x, int y){ //method 3
return (int)x+y;
}
public int add(int x, double y){ //method 4
return x+(int)y;
}
}
class Test{
public static void main(String[] args){
DemoOverload demo=new DemoOverload();
System.out.println(demo.add(2,3)); //method 1 called
System.out.println(demo.add(2,3,4)); //method 2 called
System.out.println(demo.add(2,3.4)); //method 4 called
System.out.println(demo.add(2.5,3)); //method 3 called
}
}
In
the above example, there are four versions of
add
methods. The first
method takes two parameters while the second one takes three. For the third and
fourth methods there is a change of order of parameters. The compiler
looks at the method signature and decides which method to invoke for a
particular method call at compile time.
Dynamic Polymorphism:
Suppose
a sub class overrides a particular method of the super class. Let’s say, in the
program we create an object of the subclass and assign it to the super class
reference. Now, if we call the overridden method on the super class reference
then the sub class version of the method will be called.
Have
a look at the following example.
class Vehicle{
public void move(){
System.out.println(“Vehicles can move!!”);
}
}
class MotorBike extends Vehicle{
public void move(){
System.out.println(“MotorBike can move and accelerate too!!”);
}
}
class Test{
public static void main(String[] args){
Vehicle vh=new MotorBike();
vh.move(); // prints MotorBike can move and accelerate too!!
vh=new Vehicle();
vh.move(); // prints Vehicles can move!!
}
}
It
should be noted that in the first call to
move()
, the reference type
is Vehicle
and the object being
referenced is MotorBike
. So, when a call to move()
is made, Java waits
until runtime to determine which object is actually being pointed to by the
reference. In this case, the object is of the class MotorBike
. So, themove()
method of MotorBike
class will be called.
In the second call to move()
, the object is of
the classVehicle
.
So, the move()
method of Vehicle
will be called.
As
the method to call is determined at runtime, this is called dynamic binding or late binding.
Summary:
An
object in Java that passes more than one IS-A tests is polymorphic in nature
Every
object in Java passes a minimum of two IS-A tests: one for itself and one for
Object class
Static
polymorphism in Java is achieved by method overloading
Dynamic
polymorphism in Java is achieved by method overriding
-overloaded
method MUST change the argument list
-overloaded
methods CAN change return type
-Overloaded
methods CAN change access modifiers
-Overloaded
methods CAN declare new or broader exceptions
-A
method can be overloaded in the same class or in a subclass
Method Overriding:
Occurs
when subclass declares a method that has the same type arguments as the method
declared by one of its superclasss
Use:
Define behavior that’s specific to a particular subclass type
-
CANNOT override a method marked as final
-
CANNOT override a method marked as static
Can Overloaded methods be overridden
too?
Yes,
coz compiler will not be binding the method calls since it is overloaded coz it
might be overridden now or in future.
Is
it possible to override the main methods?
No
coz main is static
How to invoke super class ver of
overrideen method
Super.overridden
mehod
Inheritance
Inheritance allows one class
to reuse the functionality provided by its superclasses. The extends clause in a
class declaration establishes an inheritance relationship between two classes.
Note that A class may directly extend only one
superclass. Each of those subclasses may itself have several subclasses.
Syntax of class to Inherit
(Inheritance)
class
clsName2 extends clsName1
{
// class body
}
clsName2 is subclass of clsName1 or clsName1 is a superclass of
clsName2.
There is an important
feature of java that relates to class hierarchies and reference variables. To
declare a variable that references an object is as below.
Syntax of Variable
declaration
clsName varName;
varName is a name of the
variable and clsName is a name of its class. So, varName
can reference any object of class clsName. Also note that varName can also
reference any object whose class is a subclass of clsName.
For example,
There is a superclass
"Person" have having subclasses "Employee" and
"Teacher". "Employee" class have subclasses
"Permanent Employee" and "Contract Employee".
As per above example Person
class reference can hold of its sub classes i.e. Employee, teacher, Permanent
Employee or Contract class object.
Note that above feature is
an important feature of java to implement run-time polymorphism.
Example of Inheritance or
Superclass - Subclass Concept
Example : Program that
illustrates inheritance in java using person class
class Person
{
String
FirstName;
String
LastName;
Person(String fName, String lName)
{
FirstName = fName;
LastName = lName;
}
void Display()
{
System.out.println("First Name : " + FirstName);
System.out.println("Last Name : " + LastName);
}
}
class Student extends Person
{
int id;
String
standard;
String
instructor;
Student(String fName, String lName, int nId, String stnd, String instr)
{
super(fName,lName);
id = nId;
standard = stnd;
instructor = instr;
}
void
Display()
{
super.Display();
System.out.println("ID : " + id);
System.out.println("Standard : " + standard);
System.out.println("Instructor : " + instructor);
}
}
class Teacher extends Person
{
String mainSubject;
int salary;
String type; // Primary or Secondary School teacher
Teacher(String fName, String lName, String sub, int slry, String
sType)
{
super(fName,lName);
mainSubject
= sub;
salary
= slry;
type
= sType;
}
void
Display()
{
super.Display();
System.out.println("Main Subject : " +
mainSubject);
System.out.println("Salary : " + salary);
System.out.println("Type : " + type);
}
}
class InheritanceDemo
{
public static void main(String args[])
{
Person pObj = new
Person("Rayan","Miller");
Student sObj = new Student("Jacob","Smith",1,"1 -
B","Roma");
Teacher tObj = new
Teacher("Daniel","Martin","English","6000","Primary
Teacher");
System.out.println("Person :");
pObj.Display();
System.out.println("");
System.out.println("Student :");
sObj.Display();
System.out.println("");
System.out.println("Teacher :");
tObj.Display();
}
}
Output
Person :
First Name : Rayan
Last Name : Miller
Student :
First Name : Jacob
Last Name : Smith
ID : 1
Standard : 1 - B
Instructor : Roma
Teacher :
First Name : Daniel
Last Name : Martin
Main Subject : English
Salary : 6000
Type : Primary
Teacher
Encapsulation:
Encapsulation(Binds): Encapsulation means binding.
Encapsulation is a process of binding or wrapping the data and its associated members into a single unit. It is also called data hiding.
Class is the best example of encapsulation.
Another example is Capsule.
Encapsulation can be implemented using private, package-private and protected access modifier.
Encapsulation(information hiding) prevents clients from seeing its inside view.
We can achieve complete encapsulation in java by making members of a class private and access them outside the class only through getters and setters.
Encapsulation is a process of binding or wrapping the data and its associated members into a single unit. It is also called data hiding.
Class is the best example of encapsulation.
Another example is Capsule.
Encapsulation can be implemented using private, package-private and protected access modifier.
Encapsulation(information hiding) prevents clients from seeing its inside view.
We can achieve complete encapsulation in java by making members of a class private and access them outside the class only through getters and setters.
Advantages:
1.
Maintenance will be
good.
2.
Encapsulation allows you to change
one part of code without affecting other part of code.
3.
By using Encapsulation we can write
immutable classes in Java.
Example:
EncapsulationDemo.java
package com.ranga;
class Student {
// data members
private long no;
private String name;
private int age;
// member functions
public long getNo() {
return no;
}
public void setNo(long no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"no=" + no +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class EncapsulationDemo
{
public static void main( String[] args )
{
Student student = new Student();
student.setNo(100);
student.setName("Ranga");
student.setAge(25);
System.out.println(student);
}
}
Output:
Student{no=100, name='Ranga', age=25}
Note: In the above Student class, we are making data members and it's associated members into a single component or single unit.
EncapsulationDemo.java
package com.ranga;
class Student {
// data members
private long no;
private String name;
private int age;
// member functions
public long getNo() {
return no;
}
public void setNo(long no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"no=" + no +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class EncapsulationDemo
{
public static void main( String[] args )
{
Student student = new Student();
student.setNo(100);
student.setName("Ranga");
student.setAge(25);
System.out.println(student);
}
}
Output:
Student{no=100, name='Ranga', age=25}
Note: In the above Student class, we are making data members and it's associated members into a single component or single unit.
Object Class:
The Object class
All classes in JavaTM technology are
directly or indirectly derived from the Object class. Some of the subclasses of
Object class are - Boolean, Number, Void, Math, String, StringBuffer etc.
Some
of the important methods defined in the Object class are given below. These
methods are available to all Java classes.
I.
boolean equals(Object obj)
- The equals method in Object class returns true if two references point to the
same object. Some classes like String and Boolean overload this method. The
difference between the equals function and the equality operator is
covered here.
II.
String toString() -
The function is used to convert objects to String. If a subclass does not
override this method, the method returns a textual representation of the
object, which has the following format : <name of the class>@<hash
code value of the object>".
III.
The following methods related to
threads are also defined in Object class -
void notify()
void notifyall()
void wait(long timeout) throws InteruptedException
void wait(long timeout, int nanosec) throws InteruptedException
void wait() throws InteruptedException
void notify()
void notifyall()
void wait(long timeout) throws InteruptedException
void wait(long timeout, int nanosec) throws InteruptedException
void wait() throws InteruptedException
Wrapper classes
Corresponding to all the primitive types Java technology
defines wrapper classes. Some examples of these wrapper classes are -
Character, Boolean, Integer, Double.
Important methods in the Math class
Some of the methods defined in the Math class are used
frequently. These are explained below. Besides the functionality, it is
important to understand the arguments and return type of these functions.
static double ceil(double(d)) : The method ceil returns the smallest double value equal to a mathematical integer, that is not less than the argument. For example,
ceil(3.4)
returns 4.0
ceil(-2.3)
returns -2.0
ceil(3.0)
returns 3.0
static double floor(double(d)) : The method floor returns the largest double value equal to a mathematical integer, that is not greater than the argument. For example,
floor(3.4)
returns 3.0
floor(-2.3)
returns -3.0
floor(3.0)
returns 3.0
static int round (float f) and static long round(double d) : The method round returns the integer closest to the argument.
round(3.7)
returns 4
round(3.2)
returns 3
round(3.0)
returns 3
round(-3.1)
returns -3
String class
The String class is used to implement immutable character
strings. This means that the character string cannot be changed once it has
been created. Some of the important methods are explained below.
int length() - The number of characters in the String class are returned by the length() method.
int length() - The number of characters in the String class are returned by the length() method.
String
substring(int startIndex)
String substring(int startIndex, int endIndex)
The method substring extracts a substring from a string. The method extracts a string from the startIndex to the index endIndex - 1. If endIndex is not specified then string till the end of input string is returned. The example below illustrates this
String substring(int startIndex, int endIndex)
The method substring extracts a substring from a string. The method extracts a string from the startIndex to the index endIndex - 1. If endIndex is not specified then string till the end of input string is returned. The example below illustrates this
String
str = "I am a string";
int
len = str.length();
String
str2 = str.substring(2,5);
After the above statements str2 contains the string "am
". The string str still has the same value "I am a string". The
variable len has value 13.
StringBuffer class
The StringBuffer class implements mutable strings. This
means that the characters stored in the string and the capacity of the string
can be changed.
Garbage Collection
Java technology's Garbage collection is complex. In this
section I am only giving a brief overview of Garbage Collection. Java
technology supports automatic garbage collection. This means that the
programmer does not need to free the memory used by objects. Java technology's
runtime environment can claim the memory from the objects that are no longer in
use. Objects that are not being referred become candidates for garbage
collection. It is important to note that these objects are candidates only.
Java technology does not guarantee that Garbage collection would happen on
these objects. Before actually freeing up the memory, garbage collector invokes
the finalize() method of the Object being freed.
The
System.gc() method can be invoked by the program to suggest to Java technology
that the Garbage Collector be invoked. However there is no guarantee when the
garbage collection would be invoked. There is also no guarantee on the order in
which objects will be garbage collected.
The
example illustrates when a string Object becomes available for Garbage
Collection.
public class GCTest {
public static void main(String args[]) {
String a,b;
String c = new String("test");
a = c;
c = null; // The String "test" is not yet
//available for GC as a still points to "test"
b = new String("xyz");
b = c; // String "xyz" is now available for GC.
a = null;
//String "test" is now available for GC.
}
}
Equals and Hashcode:
Java.lang.Object has methods called hasCode() and equals().
These methods play a significant role in the real time application. However its
use is not always common to all applications. In some case these methods are
overridden to perform certain purpose. In this article I will explain you some
concept of these methods and why it becomes necessary to override these
methods.
hashCode()
As you know this method provides the has code of an object.
Basically the default implementation of hashCode() provided by Object is
derived by mapping the memory address to an integer value. If look into the
source of Object class , you will find the following code for the hashCode.
public native int hashCode(); It indicates that hashCode is the native implementation
which provides the memory address to a certain extent. However it is possible
to override the hashCode method in your implementation class.
equals()
This particular method is used to make equal comparison
between two objects. There are two types of comparisons in Java. One is using
“= =” operator and another is “equals()”. I hope that you know the difference
between this two. More specifically the “.equals()” refers to equivalence
relations. So in broad sense you say that two objects are equivalent they
satisfy the “equals()” condition. If you look into the source code of Object
class you will find the following code for the equals() method.
public boolean equals(Object obj)
{
return (this == obj);
}
Now we will see when to override the equals() and hashCode()
methods and why it is necessary to override these methods. In this regard there
is a rule of thumb that if you are going to override the one of the methods( ie
equals() and hashCode() ) , you have to override the both otherwise it is a violation
of contract made for equals() and hashCode(). Please refer to the Sun’s java
docs for the method’s contract. I provide some test case scenario where you
will find the significance of these methods. Case-1: You can override the
hashCode method in your own way. Please refer to the following example.
package com.ddlab.core;
/**
*/
public class Emp
{
private int age ;
public Emp( int age )
{
super();
this.age = age;
}
public int hashCode()
{
return age;
}
}
In the above example class “Emp” the variable age is the
significant factor. Here the hashCode value will return the age of the person.
Now let us consider the following test harness class.
package com.ddlab.core;
/**
*/
public class TestEmp
{
public static void main(String[]
args)
{
Emp emp1 = new Emp(23);
System.out.println("emp1.hashCode()--->>>"+emp1.hashCode());
}
}
If you run the above program, the output will be the age
what you have given i.e. 23. Now question arises whether there is any way we
can get the original hashCode(). We can say that if we do not override the
hashCode() method what could have been the hashCode of this object. However
please do not feel depressed, Java provide another approach even if you have
overridden the hashCode() method , still you can get the original hashCode of a
particular class. Now run the following test harness program.
package com.ddlab.core;
package com.ddlab.core;
/**
*/
public class TestEmp
{
public static void main(String[]
args)
{
Emp emp1 = new Emp(23);
System.out.println("Overridden
hashCode()--->>>"+emp1.hashCode());
int originalHashCode = System.identityHashCode(emp1);
System.out.println("Original
hashCode of Emp---->>>"+originalHashCode);
}
}
Here the output will be like this Overridden hashCode()--->>>23
Original hashCode of Emp---->>>8567361 As you know the above number is
arbitrary, it depends upon your system. So then why it is necessary to override
this method. There is one reason that if want to compare two objects based upon
the equals() method. Although in a very simple class like “Emp”, you can
achieve without overriding hashCode() method. But if you do this , you are
going to violate the contract for the methods hashCode() and hashCode() of the
object class. The similar case is for the method equals(). So funcational point
is that if want to compare two objects based upon the equals() method you have
to override both hashCode() and equals() methods. Please have look into the Emp
class with the overridden methods and the related test harness class.
package com.ddlab.core;
/**
*/
public class Emp
{
private int age ;
public Emp( int age )
{
super();
this.age = age;
}
public int hashCode()
{
return age;
}
public boolean equals( Object
obj )
{
boolean flag = false;
Emp emp = ( Emp
)obj;
if( emp.age == age )
flag = true;
return flag;
}
}
The related test harness class is given below.
package com.ddlab.core;
/**
*/
public class TestEmp
{
public static void main(String[]
args)
{
Emp emp1 = new Emp(23);
Emp emp2 = new Emp(23);
System.out.println("emp1.equals(emp2)--->>>"+emp1.equals(emp2));
}
}
Case- 2 Think of a test scenario where you want to store
your objects in a HasSet and you want to find a particular object. First let us
see if we do not override the methods and we want to store the objects in the
HashSet. Let us analyse the impact of it from the following code.
package com.ddlab.core;
/**
*/
public class Emp
{
private int age ;
public Emp( int age )
{
super();
this.age = age;
}
}
In the above code it is a normal class. Now let us see the
test harness class.
package com.ddlab.core;
import java.util.HashSet;
/**
**/
public class TestEmp
{
public static void main(String[]
args)
{
Emp emp1 = new Emp(23);
Emp emp2 = new Emp(24);
Emp emp3 = new Emp(25);
Emp emp4 = new Emp(26);
Emp emp5 = new Emp(27);
HashSet<Emp>
hs = new HashSet<Emp>();
hs.add(emp1);
hs.add(emp2);
hs.add(emp3);
hs.add(emp4);
hs.add(emp5);
System.out.println("HashSet
Size--->>>"+hs.size());
System.out.println("hs.contains(
new Emp(25))--->>>"+hs.contains(new Emp(25)));
System.out.println("hs.remove(
new Emp(24)--->>>"+hs.remove( new Emp(24));
System.out.println("Now
HashSet Size--->>>"+hs.size());
}
}
If you run the above program, the will output will be like
the following. HashSet Size--->>>5 hs.contains( new
Emp(25))--->>>false hs.remove( new Emp(24)--->>>false Now
HashSet Size--->>>5 It means that you can not find the object. However
it is not the case for Integer object. You can put object of type Integer in a
HashSet and you can try and you can see the effect. Now let us modify the “Emp”
class so that we will get over the problems what we faced in the above test
harness class.
package com.ddlab.core;
/**
*/
public class Emp
{
private int age ;
public Emp( int age )
{
super();
this.age = age;
}
public int hashCode()
{
return age;
}
public boolean equals( Object
obj )
{
boolean flag = false;
Emp emp = ( Emp
)obj;
if( emp.age == age )
flag = true;
return flag;
}
}
Here in the above class, we have overridden the hashCode()
and equals() methods. Now if you run the same test harness class, you will get
the desired output like the following. HashSet Size--->>>5
hs.contains( new Emp(25))--->>>true hs.remove( new
Emp(24))--->>>true Now HashSet Size--->>>4 Case – 3 In this
case you want to use your object as key not the value in the HashMap. So you
have to override both the methods hashCode() and equals(). However it is left
to the reader to create the object and test the feature in a Map. Case-4 If
want to make your own immutable object , it will be wiser to override the
equals() and hashCode() methods. To test the above programs, please create the
appropriate package as mentioned in the program. You can also create your own
package and modify the package name in the above programs. You can all the code
in your favorable java editor.
Java.lang.String:
1. A Brief Summary of the String Class
A
Java
String
contains an immutable
sequence of Unicode characters. Unlike C/C++, where string is simply an array
of char
, A Java String
is an object of the
classjava.lang
.
Java
String
is, however, special.
Unlike an ordinary class:
·
String
is associated with
string literal in the form of double-quoted texts such as "Hello,
world!". You can assign a string literal directly into a String
variable, instead of calling the
constructor to create aString
instance.
·
The
'+'
operator is overloaded to concatenate
two String
operands. '+'
does not work on any other objects
such as Point
and Circle
.
·
String
is immutable. That is, its content cannot be modified
once it is created. For example, the method toUpperCase()
constructs and returns a new String
instead of modifying the its existing
content.
The
commonly-used method in the
String
class are summarized
below. Refer to the JDK API for java.lang.String
a complete listing.// Length
int length() // returns the length of the String
boolean isEmpty() // same as thisString.length == 0
// Comparison
boolean equals(String another) // CANNOT use '==' or '!=' to compare two Strings in Java
boolean equalsIgnoreCase(String another)
int compareTo(String another) // return 0 if this string is the same as another;
// <0 if lexicographically less than another; or >0
int compareToIgnoreCase(String another)
boolean startsWith(String another)
boolean startsWith(String another, int fromIndex) // search begins at fromIndex
boolean endsWith(String another)
// Searching & Indexing
int indexOf(String search)
int indexOf(String search, int fromIndex)
int indexOf(int character)
int indexOf(int character, int fromIndex) // search forward starting at fromIndex
int lastIndexOf(String search)
int lastIndexOf(String search, int fromIndex) // search backward starting at fromIndex
int lastIndexOf(int character)
int lastIndexOf(int character, int fromIndex)
// Extracting a char or part of the String (substring)
char charAt(int index) // index from 0 to String's length - 1
String substring(int fromIndex)
String substring(int fromIndex, int endIndex) // exclude endIndex
// Creating a new String or char[] from the original (Strings are immutable!)
String toLowerCase()
String toUpperCase()
String trim() // create a new String removing white spaces from front and back
String replace(char oldChar, char newChar) // create a new String with oldChar replaced by newChar
String concat(String another) // same as thisString + another
char[] toCharArray() // create a char[] from this string
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) // copy into dst char[]
// Static methods for converting primitives to String
static String ValueOf(type arg) // type can be primitives or char[]
// Static method resulted in a formatted String using format specifiers
static String format(String formattingString, Object... args) // same as printf()
// Regular Expression (JDK 1.4)
boolean matches(String regexe)
String replaceAll(String regexe, String replacement)
String replaceAll(String regexe, String replacement)
String[] split(String regexe) // Split the String using regexe as delimiter,
// return a String array
String[] split(String regexe, int count) // for count times only
Static method String.format() (JDK
1.5)
The
static
method String.format()
(introduced in JDK
1.5) can be used to produce a formatted String
using C-like printf()
's format specifiers.
The format()
method has the same
form asprintf()
.
For example,String.format("%.1f", 1.234); // returns String "1.2"
String.format()
is useful if you need
to produce a simple formatted String
for some purposes
(e.g., used in method toString()
). For complex
string, use StringBuffer
/StringBuilder
with aFormatter
. If you simply
need to send a simple formatted string to the console, use System.out.printf()
, e.g.,System.out.printf("%.1f", 1.234);
2. String is Really Special
Strings
receive special treatment in Java, because they
are used frequently in a program. Hence, efficiency (in terms of computation
and storage) is crucial.
The designers of Java
decided to retain primitive types in an object-oriented language, instead of
making everything an object, so as to improve the performance of the language.
Primitives are stored in the call stack, which require less storage spaces and
are cheaper to manipulate. On the other hand, objects are stored in the program
heap, which require complex memory management and more storage spaces.
For
performance reason, Java's
String
is designed to be in
between a primitive and a class. The special features in String
include:
·
The
Java does not support operator overloading for software engineering consideration. In a language that supports operator overloading like C++, you can turn a
'+'
operator, which performs addition on
primitives (such as int
and double
), is overloaded to
operate on String
objects. '+'
performs concatenation for two String
operands.Java does not support operator overloading for software engineering consideration. In a language that supports operator overloading like C++, you can turn a
'+'
operator to perform a subtraction,
resulted in poor codes. The '+'
operator is the only operator that is
internally overloaded to support string concatenation in Java. Take note that '+'
does not work on any two arbitrary
objects, such asPoint
s or Circle
s.
·
A
String
can be constructed by either:
1. directly assigning a
string literal to a
String
reference - just like a primitive, or
2. via the "
new
" operator and
constructor, similar to any other classes. However, this is not commonly-used
and is not recommended.
For example,
String str1 = "Java is Hot"; // Implicit construction via string literal
String str2 = new String("I'm cool"); // Explicit construction via new
In the first
statement,
str1
is declared as a String
reference and
initialized with a string literal "Java is
Hot"
.
In the second statement, str2
is declared as a String
reference and
initialized via the new
operator and
constructor to contain "I'm cool"
.
·
String
literals are stored
in a common pool. This facilitates sharing of storage for strings with the
same contents to conserve storage. String
objects allocated via new
operator are stored in the heap, and
there is no sharing of storage for the same contents.
2.1 String Literal vs. String
Object
As
mentioned, there are two ways to construct a string: implicit construction by
assigning a string literal or explicitly creating a
String
object via the new
operator and
constructor. For example,String s1 = "Hello"; // String literal
String s2 = "Hello"; // String literal
String s3 = s1; // same reference
String s4 = new String("Hello"); // String object
String s5 = new String("Hello"); // String object
Java
has provided a special mechanism for keeping the
String
literals - in a
so-called string common pool. If two string
literals have the same contents, they will share the same storage inside the
common pool. This approach is adopted to conserve storage for frequently-used
strings. On the other hand,String
objects created via
the new
operator and
constructor are kept in the heap. Each String
object in the heap
has its own storage just like any other object. There is no sharing of storage
in heap even if twoString
objects have the same
contents.
You
can use the method
equals()
of the String
class to compare the
contents of two String
s. You can use the
relational equality operator '=='
to compare the
references (or pointers) of two objects. Study the following codes:s1 == s1; // true, same pointer
s1 == s2; // true, s1 and s1 share storage in common pool
s1 == s3; // true, s3 is assigned same pointer as s1
s1.equals(s3); // true, same contents
s1 == s4; // false, different pointers
s1.equals(s4); // true, same contents
s4 == s5; // false, different pointers in heap
s4.equals(s5); // true, same contents
Important Notes:
·
In
the above example, I used relational equality operator
'=='
to compare the references of two String
objects. This is done to demonstrate
the differences between string literals sharing storage in the common pool and String
objects created in the heap. It is a logical error to
use (str1 == str2)
in
your program to compare the contents of
two String
s.
·
String
can be created by
directly assigning a String
literal which is shared in a common
pool. It is uncommon and not recommended to use the new
operator to construct a String
object in the heap.
[TODO]
Explain the method
String.intern()
.
2.2 String is Immutable
Since
string literals with the same contents share storage in the common pool, Java's
String
is designed to be immutable. That is, once a String
is constructed, its
contents cannot be modified. Otherwise, the other String
references sharing
the same storage location will be affected by the change, which can be
unpredictable and therefore is undesirable. Methods such as toUpperCase()
might appear to
modify the contents of a String
object. In fact, a
completely new String
object is created and
returned to the caller. The original String
object will be
deallocated, once there is no more references, and subsequently
garbage-collected.
Because
String
is immutable, it is
not efficient to use String
if you need to modify
your string frequently (that would create many new String
s occupying new
storage areas). For example,// inefficient codes
String str = "Hello";
for (int i = 1; i < 1000; ++i) {
str = str + i;
}
If
the contents of a
String
have to be modified
frequently, use the StringBuffer
or StringBuilder
class instead.
3. StringBuffer & StringBuilder
As
explained earlier,
String
s are immutable because String
literals with same
content share the same storage in the string common pool. Modifying the content
of one String
directly may cause
adverse side-effects to other String
s sharing the same
storage.
JDK
provides two classes to support mutable strings:
StringBuffer
and StringBuilder
(in core package java.lang
) . A StringBuffer
or StringBuilder
object is just like
any ordinary object, which are stored in the heap and not shared, and
therefore, can be modified without causing adverse side-effect to other
objects.StringBuilder
class was introduced
in JDK 1.5. It is the same as StringBuffer
class, except that StringBuilder
is not synchronized for multi-thread
operations. However, for single-thread program,StringBuilder
, without the synchronization
overhead, is more efficient.
3.1 java.lang.StringBuffer
Read
the JDK API specification for
java.lang.StringBuffer
.// Constructors
StringBuffer() // an initially-empty StringBuffer
StringBuffer(int size) // with the specified initial size
StringBuffer(String s) // with the specified initial content
// Length
int length()
// Methods for building up the content
StringBuffer append(type arg) // type could be primitives, char[], String, StringBuffer, etc
StringBuffer insert(int offset, arg)
// Methods for manipulating the content
StringBuffer delete(int start, int end)
StringBuffer deleteCharAt(int index)
void setLength(int newSize)
void setCharAt(int index, char newChar)
StringBuffer replace(int start, int end, String s)
StringBuffer reverse()
// Methods for extracting whole/part of the content
char charAt(int index)
String substring(int start)
String substring(int start, int end)
String toString()
// Methods for searching
int indexOf(String searchKey)
int indexOf(String searchKey, int fromIndex)
int lastIndexOf(String searchKey)
int lastIndexOf(String searchKey, int fromIndex)
Take
note that
StringBuffer
is an ordinary object.
You need to use a constructor to create a StringBuffer
(instead of assigning
to a String
literal).
Furthermore, '+'
operator does not
apply to objects, including the StringBuffer
. You need to use a
proper method such as append()
or insert()
to manipulating a StringBuffer
.
To
create a string from parts, It is more efficient to use
StringBuffer
(multi-thread) or StringBuilder
(single-thread)
instead of via String
concatenation. For
example,// Create a string of YYYY-MM-DD HH:MM:SS
int year = 2010, month = 10, day = 10;
int hour = 10, minute = 10, second = 10;
String dateStr = new StringBuilder()
.append(year).append("-").append(month).append("-").append(day).append(" ")
.append(hour).append(":").append(minute).append(":").append(second).toString();
System.out.println(dateStr);
// StringBuilder is more efficient than String concatenation
String anotherDataStr = year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
System.out.println(anotherDataStr);
JDK
compiler, in fact, uses both
String
and StringBuffer
to handle string
concatenation via the '+'
operator. For
examples,String msg = "a" + "b" + "c";
will be compiled into
the following codes for better efficiency:
String msg = new StringBuffer().append("a").append("b").append("c").toString();
Two
objects are created during the process, an intermediate
StringBuffer
object and the
returned String
object.
Rule of Thumb:
String
s are more efficient if they are not
modified (because they are shared in the string common pool). However, if you
have to modify the content of a string frequently (such as a status message),
you should use the StringBuffer
class (or the StringBuilder
described below)
instead.
3.2 java.lang.StringBuilder (JDK 1.5)
JDK
1.5 introduced a new
StringBuilder
class (in package java.lang
), which is almost
identical to the StringBuffer
class, except that it
is not synchronized. In other words, if
multiple threads are accessing a StringBuilder
instance at the same
time, its integrity cannot be guaranteed. However, for a single-thread program
(most commonly), doing away with the overhead of synchronization makes theStringBuilder
faster.StringBuilder
is API-compatible
with the StringBuffer
class, i.e., having
the same set of constructors and methods, but with no guarantee of
synchronization. It can be a drop-in replacement forStringBuffer
under a single-thread environment.
3.3 Benchmarking String/StringBuffer/StringBuilder
The
following program compare the times taken to reverse a long string via a
String
object and a StringBuffer
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// Reversing a long String via a String vs. a StringBuffer
public class StringsBenchMark {
public static void main(String[] args) {
long beginTime, elapsedTime;
// Build a long string
String str = "";
int size = 16536;
char ch = 'a';
beginTime = System.nanoTime(); // Reference time in nanoseconds
for (int count = 0; count < size; ++count) {
str += ch;
++ch;
if (ch > 'z') {
ch = 'a';
}
}
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Build String)");
// Reverse a String by building another String character-by-character in the reverse order
String strReverse = "";
beginTime = System.nanoTime();
for (int pos = str.length() - 1; pos >= 0 ; pos--) {
strReverse += str.charAt(pos); // Concatenate
}
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Using String to reverse)");
// Reverse a String via an empty StringBuffer by appending characters in the reverse order
beginTime = System.nanoTime();
StringBuffer sBufferReverse = new StringBuffer(size);
for (int pos = str.length() - 1; pos >= 0 ; pos--) {
sBufferReverse.append(str.charAt(pos)); // append
}
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Using StringBuffer to reverse)");
// Reverse a String by creating a StringBuffer with the given String and invoke its reverse()
beginTime = System.nanoTime();
StringBuffer sBufferReverseMethod = new StringBuffer(str);
sBufferReverseMethod.reverse(); // use reverse() method
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Using StringBuffer's reverse() method)");
// Reverse a String via an empty StringBuilder by appending characters in the reverse order
beginTime = System.nanoTime();
StringBuilder sBuilderReverse = new StringBuilder(size);
for (int pos = str.length() - 1; pos >= 0 ; pos--) {
sBuilderReverse.append(str.charAt(pos));
}
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Using StringBuilder to reverse)");
// Reverse a String by creating a StringBuilder with the given String and invoke its reverse()
beginTime = System.nanoTime();
StringBuffer sBuilderReverseMethod = new StringBuffer(str);
sBuilderReverseMethod.reverse();
elapsedTime = System.nanoTime() - beginTime;
System.out.println("Elapsed Time is " + elapsedTime/1000 + " usec (Using StringBuidler's reverse() method)");
}
}
|
Elapsed Time is 332100 usec (Build String)
Elapsed Time is 346639 usec (Using String to reverse)
Elapsed Time is 2028 usec (Using StringBuffer to reverse)
Elapsed Time is 847 usec (Using StringBuffer's reverse() method)
Elapsed Time is 1092 usec (Using StringBuilder to reverse)
Elapsed Time is 836 usec (Using StringBuidler's reverse() method)
Observe
StringBuilder
is 2x faster than StringBuffer
, and 300x faster
than String
. The reverse()
method is the
fastest, which take about the same time for StringBuilder
and StringBuffer
.
4. java.util.StringTokenizer
Very
often, you need to break a line of texts into tokens delimited by white spaces.
The
java.util.StringTokenizer
class supports this.
For example, the
following program reverses the words in a String.
// Reverse the words in a String using StringTokenizer
import java.util.StringTokenizer;
public class StringTokenizerTest {
public static void main(String[] args) {
String str = "Monday Tuesday Wednesday Thursday Friday Saturday Sunday";
String strReverse;
StringBuilder sb = new StringBuilder();
StringTokenizer st = new StringTokenizer(str);
while (st.hasMoreTokens()) {
sb.insert(0, st.nextToken());
if (st.hasMoreTokens()) {
sb.insert(0, " ");
}
}
strReverse = sb.toString();
System.out.println(strReverse);
}
}
// Constructors
StringTokenizer(String s) // Constructs a StringTokenizer for the given string,
// using the default delimiter set of " \t\n\r\f"
// (i.e., blank, tab, newline, carriage-return, and form-feed).
// Delimiter characters themselves will not be treated as tokens.
StrintTokenizer(String s, String delimiterSet) // Use characters in delimiterSet as delimiters.
// Methods
boolean hasNextToken() // Returns true if next token available
String nextToken() // Returns the next token
// Code Sample
StringTokenizer tokenizer = new StringTokenizer(aString);
while (tokenizer.hasNextToken()) {
String token = tokenizer.nextToken();
.....
}
The
JDK documentation stated that "
StringTokenizer
is a legacy class
that is retained for compatibility reasons although its use is discouraged in
new code. It is recommended that anyone seeking this functionality use the split()
method of String
or the java.util.regex
package
instead."
For example, the
following program uses the split() method of the String class to reverse the
words of a String.
// Reverse the words in a String using split() method of the String class
public class StringSplitTest {
public static void main(String[] args) {
String str = "Monday Tuesday Wednesday Thursday Friday Saturday Sunday";
String[] tokens = str.split("\\s"); // white space '\s' as delimiter
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tokens.length; ++i) {
sb.insert(0, tokens[i]);
if (i < tokens.length - 1) {
sb.insert(0, " ");
}
}
String strReverse = sb.toString();
System.out.println(strReverse);
}
}
Java Serialization:
Java
Object Serialization, introduced as part of the ground-breaking feature set
that made up JDK 1.1, serves as a mechanism to transform a graph of Java
objects into an array of bytes for storage or transmission, such that said
array of bytes can be later transformed back into a graph of Java objects.
In
essence, the idea of Serialization is to "freeze" the object graph,
move the graph around (to disk, across a network, whatever), and then
"thaw" the graph back out again into usable Java objects. All this
happens more or less magically, thanks to the
ObjectInputStream
/ObjectOutputStream
classes,
full-fidelity metadata, and the willingness of programmers to "opt
in" to this process by tagging their classes with the Serializable
marker interface.
Listing
1 shows a
Person
class implementing Serializable
.
Listing 1. Serializable Person
package com.tedneward;
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
this.firstName = fn; this.lastName = ln; this.age = a;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" age=" + age +
" spouse=" + spouse.getFirstName() +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
}
Once
Person
has been serialized,
it's pretty simple to write an object graph to disk and read it back again, as
demonstrated by this JUnit 4 unit test.
Listing 2. Deserializing Person
public class SerTest
{
@Test public void serializeToDisk()
{
try
{
com.tedneward.Person ted = new com.tedneward.Person("Ted", "Neward", 39);
com.tedneward.Person charl = new com.tedneward.Person("Charlotte",
"Neward", 38);
ted.setSpouse(charl); charl.setSpouse(ted);
FileOutputStream fos = new FileOutputStream("tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(ted);
oos.close();
}
catch (Exception ex)
{
fail("Exception thrown during test: " + ex.toString());
}
try
{
FileInputStream fis = new FileInputStream("tempdata.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
com.tedneward.Person ted = (com.tedneward.Person) ois.readObject();
ois.close();
assertEquals(ted.getFirstName(), "Ted");
assertEquals(ted.getSpouse().getFirstName(), "Charlotte");
// Clean up the file
new File("tempdata.ser").delete();
}
catch (Exception ex)
{
fail("Exception thrown during test: " + ex.toString());
}
}
}
Nothing
you've seen so far is new or exciting — it's Serialization 101 — but it's a
good place to start. We'll use
Person
to discover five
things you probably didn't already know about Java Object Serialization.
1. Serialization allows for refactoring
Serialization permits a certain amount of
class variation, such that even after refactoring,
ObjectInputStream
will still read it
just fine.
The
critical things that the Java Object Serialization specification can
manage automatically are:
·
Adding
new fields to a class
·
Changing
the fields from static to nonstatic
·
Changing
the fields from transient to nontransient
Going
the other way (from nonstatic to static or nontransient to transient) or deleting
fields requires additional massaging, depending on the degree of backward
compatibility you require.
Refactoring
a serialized class
Knowing that Serialization allows for
refactoring, let's see what happens when we decide to add a new field to the
Person
class.PersonV2
, shown in Listing 3,
introduces a field for gender to the original Person
class.
Listing 3. Adding a new field to serialized Person
enum Gender
{
MALE, FEMALE
}
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a, Gender g)
{
this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public Gender getGender() { return gender; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setGender(Gender value) { gender = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" gender=" + gender +
" age=" + age +
" spouse=" + spouse.getFirstName() +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
private Gender gender;
}
Serialization
uses a calculated hash based on just about everything in a given source file —
method names, field names, field types, access modifiers, you name it — and
compares that hash value against the hash value in the serialized stream.
To
convince the Java runtime that the two types are in fact the same, the second
and subsequent versions of
Person
must have the same
serialization version hash (stored as the private static final serialVersionUID
field) as the first
one. What we need, therefore, is theserialVersionUID
field, which is
calculated by running the JDK serialver
command against the
original (or V1) version of the Person
class.
Once
we have
Person
's serialVersionUID
, not only can we
create PersonV2
objects out of the
original object's serialized data (where the new fields appear, they will
default to whatever the default value is for a field, most often
"null"), but the opposite is also true: we can deserialize originalPerson
objects out of PersonV2
data, with no added
fuss.
2. Serialization is not secure
It often comes as an unpleasant surprise to
Java developers that the Serialization binary format is fully documented and
entirely reversible. In fact, just dumping the contents of the binary
serialized stream to the console is sufficient to figure out what the class
looks like and contains.
This
has some disturbing implications vis-a-vis security. When making
remote method calls via RMI, for example, any private fields in the objects
being sent across the wire appear in the socket stream as almost plain-text,
which clearly violates even the simplest security concerns.
Fortunately,
Serialization gives us the ability to "hook" the serialization
process and secure (or obscure) the field data both before serialization and
after deserialization. We can do this by providing a
writeObject
method on a Serializable
object.
Obscuring serialized
data
Suppose the sensitive data in the
Person
class were the age
field; after all, a lady never reveals her age and a gentleman never tells. We
can obscure this data by rotating the bits once to the left before
serialization, and then rotate them back after deserialization. (I'll leave it
to you to develop a more secure algorithm, this one's just for example's sake.)
To
"hook" the serialization process, we'll implement a
writeObject
method on Person
; and to
"hook" the deserialization process, we'll implement a readObject
method on the same
class. It's important to get the details right on both of these — if the access
modifier, parameters, or name are at all different from what's shown in Listing
4, the code will silently fail, and our Person
's age will be
visible to anyone who looks.
Listing 4. Obscuring serialized data
public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
this.firstName = fn; this.lastName = ln; this.age = a;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
// "Encrypt"/obscure the sensitive data
age = age >> 2;
stream.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException
{
stream.defaultReadObject();
// "Decrypt"/de-obscure the sensitive data
age = age << 2;
}
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" age=" + age +
" spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
}
If
we need to see the obscured data, we can always just look at the serialized
data stream/file. And, because the format is fully documented, it's possible to
read the contents of the serialized stream without the class being available.
3. Serialized data can be signed and sealed
The previous tip assumes that you want to
obscure serialized data, not encrypt it or ensure it hasn't been modified.
Although cryptographic encryption and signature management are certainly possible
using
writeObject
and readObject
, there's a better
way.
If
you need to encrypt and sign an entire object, the simplest thing is to put it
in a
javax.crypto.SealedObject
and/orjava.security.SignedObject
wrapper. Both are
serializable, so wrapping your object in SealedObject
creates a sort of
"gift box" around the original object. You need a symmetric key to do
the encryption, and the key must be managed independently. Likewise, you can
useSignedObject
for data
verification, and again the symmetric key must be managed independently.
Comments
Post a Comment