CS F213 Objected Oriented Programming Labsheet 10

Spring 2024

Java NIO

Java.nio package was introduced in java 1.4. In contrast of java I/O in java NIO the buffer and channel oriented data flow for I/O operations is introduced which in result provide faster execution and better performance.

The central abstractions of the NIO APIs are following −

  • Channels of various types,which represent connections to entities capable of performing I/O operations

  • Buffers which are containers for data,charsets and their associated decoders and encoders,which translate between bytes and Unicode characters.

  • Selectors and selection keys, which together with selectable channels define a multiplexed, non-blocking I/O facility.

NIO- Channels

As name suggests channel is used as mean of data flow from one end to other.Here in java NIO channel act same between buffer and an entity at other end in other words channel are use to read data to buffer and also write data from buffer.

Java NIO channel is implemented primarily in following classes:

  1. FileChannel : used for reading the data from the files. It's object can be created only by calling the getChannel() method. We cannot create FileChannel object directly.

FileInputStream fis = new FileInputStream("D:\\testin.txt"); // Path of Input text file  
ReadableByteChannel rbc = fis.getChannel();  
  1. DatagramChannel : The datagram channel can read and write the data over the network via UDP (User Datagram Protocol).

DatagramChannel ch = DatagramChannel.open();  
DatagramChannel ch = DatagramChannel.close();
  1. SocketChannel : The Socket channel can read and write the data over the network via TCP (Transmission Control Protocol).

SocketChannel ch = SocketChannel.open();  
ch.connect(new InetSocketAddress("somehost", someport)); 
  1. ServerSocketChannel: The ServerSocketChannel allows user to listen the incoming TCP connections, same as a web server.

ServerSocketChannel ch = ServerSocketChannel.open();  
ch.socket().bind (new InetSocketAddress (somelocalport));  

NIO- Buffers

Java NIO buffers are used for interacting with NIO channels. It is the block of memory into which we can write data, which we can later be read again. The memory block is wrapped with a NIO buffer object, which provides easier methods to work with the memory block.

In NIO the data transfer take place by using the buffers implemented in java.nio.Buffer class. It is similar to array, and having a fixed capacity.

In Java NIO the core Buffer used are given below:

  • CharBuffer

  • DoubleBuffer

  • IntBuffer

  • LongBuffer

  • ByteBuffer

  • ShortBuffer

  • FloatBuffer

Allocating a Buffer

//allocation of ByteBuffer, with capacity of 28 bytes:
ByteBuffer buf = ByteBuffer.allocate(28);  
//allocation of CharBuffer, with space for 2048 characters:
CharBuffer buf = CharBuffer.allocate(2048);  

Reading Data from a Buffer

There are two methods for reading the data from a Buffer:

  1. Read the data from Buffer by using one of the get() method.

byte aByte = buf.get();  
  1. Read the data from Buffer into a Channel.

int bytesWritten = inChannel.write(buf);  

Writing Data to a Buffer

Similarly there are two methods for writing the data into a Buffer:

  1. Write the data into Buffer by using one of the put() method.

  2. Write the data from Channel into a Buffer.

Example

import java.util.*; 
import java.nio.*;
import java.nio.channels.ReadableByteChannel;  
import java.io.*;
class Main3 { 
	public static void main(String[] args) 
	{ 
        try{
            FileInputStream fis = new FileInputStream("source.txt"); // Path of Input text file  
            ReadableByteChannel fileChannel = fis.getChannel();  
            //FileChannel fileChannel = FileChannel.open(Paths.get("source.txt"), READ);
            ByteBuffer byteBuffer = ByteBuffer.allocate(8);
            // Then, read the content of the file to fill the ByteBuffer
            fileChannel.read(byteBuffer);
            // Then, we flip the ByteBuffer and print it
            byteBuffer.flip();
            System.out.println( 
				"Original ByteBuffer: "
				+ Arrays.toString(byteBuffer.array())); 
        }
        catch (Exception e) { 
			e.printStackTrace(); 
		}
    }
}

source.txt 1 2 3

Output

Original ByteBuffer: [49, 32, 50, 32, 51, 0, 0, 0]

Explanation of the output:

  • The ASCII value of '1' is 49, '2' is 50, and '3' is 51.

  • The ASCII value of space is 32.

  • The ByteBuffer is allocated with a capacity of 8 bytes, so it reads the first 8 bytes from the file, which include the ASCII values of '1', space, '2', and space and so on.

  • Since the buffer is flipped, it prints all 8 bytes stored in it. The last few zeros indicate unused space in the buffer.

Java Assertions

Assertions in Java help to detect bugs by testing code we assume to be true. An assertion is made using the assert keyword. Its syntax is:

assert condition;
//OR
assert condition : expression;

Here, condition is a boolean expression that we assume to be true when the program executes.

Enabling Assertions

By default, assertions are disabled and ignored at runtime. To enable assertions, we use:

java -ea ClassName
//OR
java -enableassertions ClassName

When assertions are enabled and the condition is true, the program executes normally. But if the condition evaluates to false while assertions are enabled, JVM throws an AssertionError, and the program stops immediately.

Example

class Main {
  public static void main(String args[]) {
    String[] weekends = {"Friday", "Saturday", "Sunday"};
    assert weekends.length == 2 : "There are only 2 weekends in a week";
    System.out.println("There are " + weekends.length + "  weekends in a week");
  }
}

Output

blue@Blueberry:~/ubuntu$ javac Main.java

blue@Blueberry:~/ubuntu$ java Main
There are 3  weekends in a week

blue@Blueberry:~/ubuntu$ java -ea Main
Exception in thread "main" java.lang.AssertionError: There are only 2 weekends in a week
        at Main.main(Main.java:4)

Advantages of Assertion

  • Quick and efficient for detecting and correcting bugs.

  • Assertion checks are done only during development and testing. They are automatically removed in the production code at runtime so that it won’t slow the execution of the program.

  • It helps remove boilerplate code and make code more readable.

  • Refactors and optimizes code with increased confidence that it functions correctly.

Java instanceOf

The java instanceof operator is used to test whether the object is an instance of the specified type (class or subclass or interface).

The instanceof in java is also known as type comparison operator because it compares the instance with type. It returns either true or false. If we apply the instanceof operator with any variable that has null value, it returns false.

class Animal {
}

// subclass
class Dog extends Animal {
}
class Main2 {
  public static void main(String[] args) {

    // create an object of the subclass
    Dog d1 = new Dog();
    Computer c1 = new Computer();
    // checks if d1 is an instance of the subclass
    System.out.println(d1 instanceof Dog);
    // checks if d1 is an instance of the superclass
    System.out.println(d1 instanceof Animal);   
  }
}
true
true

Note: In Java, all the classes are inherited from the Object class. So, instances of all the classes are also an instance of the Object class.

Java Records

In Java, a record is a special type of class declaration aimed at reducing the boilerplate code. Java records were introduced with the intention to be used as a fast way to create data carrier classes, i.e. the classes whose objective is to simply contain data and carry it between modules.

Consider a simple class Employee, whose objective is to contain an employee’s data such as its ID and name and act as a data carrier to be transferred across modules. To create such a simple class, you’d need to define its constructor, getter, and setter methods, and if you want to use the object with data structures like HashMap or print the contents of its objects as a string, we would need to override methods such as equals(), hashCode(), and toString(). Instead of writing over 80 lines of code for this we can write :

public record Employee(int id, String firstName, String lastName) {}

The constructor, getter methods, toString(), equals(), and hashCode() are generated by the Java compiler during compile time. One thing to note here is that records do not provide setter methods, as it is expected that the value to instance variables is provided while creating the object.

Records also provide us the capability to:

  • Create our own constructors. In records, you can create a parameterized constructor, which calls the default constructor with the provided parameters inside its body.

  • Create instance methods. Like any other class, you can create and call instance methods for the record class.

  • Create static fields. Records restrict us to write the instance variables only as parameters but enable the use of static variables and static methods

Example

package org.example;
import java.lang.Record;
import java.util.Objects;
record Person(int id, String name){}
public class Main2
{
    public static void main( String[] args )
    {
        Person p1 = new PersonRecord(1,"Peter Parker");
        Person p2 = new PersonRecord(2,"Spiderman");
        System.out.println(p1.equals(p2));
        System.out.println(p1.name());
        System.out.println(p2.id());
    }
}
false
Peter Parker
2

Java Sealed Classes

In Java, class hierarchies can become complex, with numerous subclasses extending a common superclass. This can make it challenging to maintain and control the hierarchy. Unintentional extensions and modifications of classes can lead to unexpected issues and bugs in the codebase.

The sealed modifier is used to declare a class as sealed. Additionally, the classes that are permitted to be its direct subclasses are specified using the permits keyword. Here’s an example:

public sealed class Animal permits Dog, Cat, Bird {
    //Class implementation
}

In this example, the Animal class is declared as sealed and permits three subclasses: Dog, Cat and Bird. Any attempt to create a new subclass of Animal outside of this list will result in a compilation error.

Example

public sealed interface Service permits Car, Truck {
    int getMaxServiceIntervalInMonths();
    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }
}
public abstract sealed class Vehicle permits Car, Truck {
    protected final String registrationNumber;
    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }
    public String getRegistrationNumber() {
        return registrationNumber;
    }
public final class Truck extends Vehicle implements Service {
    private final int loadCapacity;
    public Truck(int loadCapacity, String registrationNumber) {
        super(registrationNumber);
        this.loadCapacity = loadCapacity;
    }
    public int getLoadCapacity() {
        return loadCapacity;
    }
    @Override
    public int getMaxServiceIntervalInMonths() {
        return 18;
    }
}
}

Java Records and Sealed Classes are only available in the later versions of java so you might need an IDE like Eclipse to access these features.

Java Generics

Java Generics allows us to create a single class, interface, and method that can be used with different types of data (objects). This helps us to reuse our code.

Note: Generics does not work with primitive types (int, float, char, etc).

Java Generics Class We can create a class that can be used with any type of data. Such a class is known as Generics Class. Here's is how we can create a generics class in Java:

Example

class Main {
  public static void main(String[] args) {

    // initialize generic class with Integer data
    GenericsClass<Integer> intObj = new GenericsClass<>(5);
    System.out.println("Generic Class returns: " + intObj.getData());
    // initialize generic class with String data
    GenericsClass<String> stringObj = new GenericsClass<>("Java Programming");
    System.out.println("Generic Class returns: " + stringObj.getData());
  }
}
// create a generics class
class GenericsClass<T> {
  // variable of T type
  private T data;
  public GenericsClass(T data) {
    this.data = data;
  }
  // method that return T type variable
  public T getData() {
    return this.data;
  }
}
Generic Class returns: 5
Generic Class returns: Java Programming

Here, T used inside the angle bracket <> indicates the type parameter.

Exercises

Exercise 1

Create a Java program that reads data from a source file using Java NIO and verifies its content using the assert keyword. For this exercise let the content be "Hello, World!". Note that the size is 13 bytes, so read the content of the file, use a ByteBuffer with a suitable capacity, verify that the content read from the file matches an expected value by using the assert keyword, if the content matches the expected value, print a message indicating successful verification. If not, print an error message.

Sample Output

source.txt : Hello, World!

Content verified successfully: Hello, World!

source.txt : Hello, Worrd!!

Exception in thread "main" java.lang.AssertionError: Content mismatch!
        at Ex1.main(Ex1.java:19)

Exercise 2

Create a program that represents different types of shapes - Circle, Rectangle, and Triangle using a sealed class Shape. Implement methods to calculate the area of each shape. Then, create an application that calculates the total area of all shapes and prints it.

  • Define a sealed abstract class Shape with subclasses Circle, Rectangle, and Triangle and an abstract method calcArea().

  • Each subclass should have appropriate fields and override the calcArea() method to calculate its area.

  • Create an application (Main) that creates instances of different shapes by creating an array of 3 shapes and calculates the total area of all shapes.

  • Use the instanceof keyword to check the type of each shape by if-else ladder, calculate its area, total area and print accordingly.

One shape (circle) class is below for reference

final class Circle extends Shape {
  private double radius;
  public Circle(double radius) {
      this.radius = radius;
  }
  @Override
  double calculateArea() {
      return Math.PI * radius * radius;
  }
}

in Main you can use the following code for getting the name of the class after you checked by instanceOf shape.getClass().getSimpleName() + "'s area: " + area

Sample Input (in the code)

Shape[] shapes = new Shape[]{
                new Circle(5),
                new Rectangle(4, 3),
                new Triangle(6, 2)
        };

Sample Output

Circle's area: 78.53981633974483
Rectangle's area: 12.0
Triangle's area: 6.0
Total area of all shapes: 96.53981633974483

Exercise 3

Design a program to manage student records using Java records. Each student record should contain the following information:

  1. Student ID

  2. Student Name

  3. List of Course IDs the student is enrolled in

  4. GPA (Grade Point Average)

Implement methods to perform the following operations:

  1. Add a new student record.

  2. Remove a student record by ID.

  3. Update a student's GPA by ID.

  4. Display all student records.

  5. Calculate and display the average GPA of all students.

You should use the methods provided by record and not make all the methods yourself. You cannot create your own setter methods for record components. This is because records are designed to be immutable data holders, and their components are automatically final. Therefore, you cannot modify the values of record components after the record instance has been created.

If you need to modify the values of the record components, you would need to create a new record instance with the updated values.

Sample Input (in the code)

manager.addStudent(new StudentRecord(1, "John Doe", 3.5, new int[]{101, 102}));
manager.addStudent(new StudentRecord(2, "Alice Smith", 3.8, new int[]{101}));
manager.addStudent(new StudentRecord(3, "Bob Johnson", 2.9, new int[]{102}));
manager.displayAllStudents();
manager.removeStudentById(2);
manager.displayAllStudents();
manager.updateGpaById(1,4.0);
manager.displayAllStudents();
double avgGpa = manager.calculateAverageGpa();
System.out.println("Average GPA of all students: " + avgGpa);

Sample Output

Student Records:
Student ID: 1, Name: John Doe, Course IDs: [101, 102], GPA: 3.5
Student ID: 2, Name: Alice Smith, Course IDs: [101], GPA: 3.8
Student ID: 3, Name: Bob Johnson, Course IDs: [102], GPA: 2.9
Student with id = 2 removed
Student Records:
Student ID: 1, Name: John Doe, Course IDs: [101, 102], GPA: 3.5
Student ID: 3, Name: Bob Johnson, Course IDs: [102], GPA: 2.9
GPA updated for Student with id = 1
Student Records:
Student ID: 1, Name: John Doe, Course IDs: [101, 102], GPA: 4.0
Student ID: 3, Name: Bob Johnson, Course IDs: [102], GPA: 2.9
Average GPA of all students: 3.45

Exercise 4

Create a generic class called Pair<K, V> that represents a simple key-value pair. Implement methods to set and get the key and value of the pair.

Instructions:

  • Define a generic class called Pair<K, V> with two type parameters K for the key and V for the value.

  • Provide methods to set and get the key and value of the pair.

  • Test your Pair<K, V> class by creating instances of it with different types for the key and value (e.g., Integer, String, String, Double) and performing various operations like setting and getting the key and value.

Sample Input (in the code)

Pair<String, Double> pair2 = new Pair<>("Pi", 3.14);
Pair<Character, Boolean> pair3 = new Pair<>('A', true);

Sample Output

Key: Pi, Value: 3.14
Key: A, Value: true

Last updated