paint-brush
Java Essentials: A Quick Guide for Refreshing Developers' Knowledgeby@mrdrseq
1,450 reads
1,450 reads

Java Essentials: A Quick Guide for Refreshing Developers' Knowledge

by Ilia IvankinNovember 15th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The material is designed for developers with little experience with Java and the main reason is to refreshing their knowledge. Java interview and Java 8+.
featured image - Java Essentials: A Quick Guide for Refreshing Developers' Knowledge
Ilia Ivankin HackerNoon profile picture


Who is this article for?

The material is designed for developers with little experience with Java, and the main reason is to refresh their knowledge.


Table of Contents:

  1. Variables and types
  2. It will be better to use these
  3. Switch and case. Try, catch, and finally.
  4. Lambdas, streams, optional?
  5. Record class



Variables and types.

Let’s start with the general and important things that you really should have learned earlier. Java, like many languages, has two types of variables:

Primitives:

You could see these in your old book about java :D

Non-Primitive: Classes and arrays!


Usually, we need to compare variables, but what methods should we use? “equals” or “== “ ? Let’s look at all the comparison methods available to us:

==      equal to
!=      not equal to
>       greater than
>=      greater than or equal to
<       less than
<=      less than or equal to


This tricky question was asked in the interview. We can use it with classes, but:

// Integer != int
int a = 1, b = 1;
System.out.println(a == b); // true

int c = 1, j = c;
System.out.println(c == j); // true

Integer ai = Integer.valueOf(1), bi = Integer.valueOf(1);
System.out.println(ai == bi); // true 

Integer ci = Integer.valueOf(128), ji = Integer.valueOf(128);
System.out.println(ci == ji); // false
/* You can always count on the fact that 
*  for values between -128 and 127, 
*  you get the identical Integer objects after autoboxing, 
*  and on some implementations you might get identical objects
*  even for higher values.
*/


It’s important to learn that the Integer class is not a primitive value because it is a regular class. Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.


The “==” operator compares the values of two object references to see if they refer to the same thing. For an object, we should use “equals”:

// Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj)


Or Comparable interface for natural ordering (if you need to sort your collection with your own type of object):

public class HugePoint implements Comparable<HugePoint> {

    // same as before

    @Override
    public int compareTo(HugePoint otherHugePoint) {
        return Integer.compare(getSize(), otherHugePoint.getSize());
    }

}


The Comparator interface defines a compare(arg1, arg2) method:


public class HugePointComparator implements Comparator<HugePoint> {

    @Override
    public int compare(HugePoint firstPoint, HugePoint secondPoint) {
       return Integer.compare(firstPoint.getSize(), secondPoint.getSize());
    }

}


It’s easy to use Java 8+ this way:

Comparator<Person> byAge = Comparator.comparing(Person::getAge);

Comparator byAge = (Person person1, Person person2) -> 
    Integer.compare(person1.getAge(), person2.getAge());


Which one do you need to use and when? I would recommend using “Comparable” interface for ordering (if you have a sorting method, etc.), and “Comparator” if you need to compare objects by a special field.


It will be better to use these

Date and time

Why? For the current timestamp, just use Instant.now(). We don’t need to convert to milliseconds.

// Instant class, one of the main classes of the Date-Time API, encapsulates a point on the timeline.
Instant.now();
Instant now = Instant.parse("2021-02-09T11:19:42.12Z");
Instant before = now.minus(Duration.ofDays(1));

For the local date format use the following:

// LocalDateTime is an immutable date-time object 
// that represents a date-time, often viewed 
// as year-month-day-hour-minute-second. 
LocalDateTime.now();
LocalDateTime.of(2001, Month.FEBRUARY, 10, 03, 01);


UUID

// uuid
UUID uuid = UUID.randomUUID();
UUID uuid = new UUID(mostSignificant64Bits, leastSignificant64Bits);


Random

// random for int
int randomWithMathRandom = (int) ((Math.random() * (max - min)) + min);

// or easy way
Random random = new Random();
int randomWithNextInt = random.nextInt();

Switch and case. Try, catch, and finally

When we have too many conditions, and they are all tied to the same type of variable in “if-then-else,” it is worth using a switch-case.

// case 
int expression = 9;
switch(expression) {
  case 2:
    System.out.println("Small Size");
    break;

  case 3:
    System.out.println("Large Size");
    break;
        
  // default case
  default:
    System.out.println("Unknown Size");
}

Also, up Java to version 19 or more, and we can use the expression:

Day day = Day.WEDNESDAY;
var resultDay = switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> 6;
        case TUESDAY                -> 7;
        case THURSDAY, SATURDAY     -> 8;
        case WEDNESDAY              -> 9;
        default -> throw new IllegalStateException("Invalid day: " + day);
    } 

System.out.println(resultDay);


Don’t forget to use try/catch with methods that can throw exceptions. Try/catch and try with resources:

// old
public int find(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        throw new IllegalArgumentException("Couldn't find the file");
    }
}

// with resources
public int find(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
      return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e ) {
      logger.warn("Couldn't find the file");
      return 0;
    }
}

// don't do it!
public int find(String playerFile) {
    try {
        // action
    } catch (Exception e) {} // <== catch and swallow
    return 0;
}


Use finally, especially with channels:

// close it!
public int find(String playerFile) throws FileNotFoundException {
    Scanner contents = null;

    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } finally {
        if (contents != null) {
            contents.close();
        }
    }
}

// Always close it!
public int find(String playerFile){
    MessageChannel channel = buildChannel(port);
    try {
      // do some action with channel
    } finally {
      channel.close(); // close!
    }
}



Lambdas, streams, optional?

The main reason for switching to Java 8 and higher is to be able to work with threads, streams, and functional interfaces. In this article, we will cover only working with streams, and we will deal with threads on the next page.

First point — collections:

Also, it would be good to go through the algorithms’ complexity table. I strongly recommend using this link: gist or the full article here.


Let’s analyze the two approaches and see the difference.

// old way: 
List<String> old = new ArrayList<>();
old.add("one");

// new way:
List<String> list = Arrays.asList("one", "two");
List<String> list = List.of("one", "two", "three");
Set<String> set = Set.of("one", "two", "three");


We can simplify working with the map.

// old:
HashMap<Integer, String> hm = new HashMap<Integer, String>();
hm.put(1, "foo");

// new:
Map<String, String> map = Map.of("foo", "one", "bar", "two");


As you know, we have only two types of operations: intermediate and terminal operations.

// Intermediate Operations
var stream = ...;
stream.filter();
stream.map();
stream.flatMap();
stream.distinct();
stream.sorted();
stream.peek();
stream.limit();
stream.skip();


// Terminal
stream.reduce();
stream.forEach();
stream.min();
stream.max();
stream.anyMatch();
stream.allMatch();
stream.noneMatch();
stream.findAny();
stream.findFirst();
stream.toArray();
stream.collect();
stream.count();
stream.forEachOrdered();


Just code:

// Java 7 way
ArrayList results = new ArrayList(); 
for (Score score: scores) { 
  if (score.getVal() > 10) { 
      results.add(score.getVal()); 
   } 
} 
System.out.println(results);

// Java 8+ way
ArrayList results = scores.stream()
      .filter(score -> score.getVal() > 10)
      .collect(Collectors.toList()); 

System.out.println(results);


If you have a duplicate code, just use functional interfaces:

// Functional 
Consumer<Integer> consumer = (n) -> { System.out.println(n); };
numbers.forEach(consumer);


Throwing exceptions every time an object is not found is not a good idea; you should use the “Optional” class for this:

var empty = Optional.empty();
empty.isPresent(); // false

var notEmpty = Optional.of(name);
notEmpty.isPresent(); // true

String name = null;
Optional<String> opt = Optional.ofNullable(name);
opt.isPresent(); // false

String name = null;
String result = Optional.ofNullable(nullName).orElse("Default");

// get result
var name = Optional.of("Name");
name.get();


And if you really need to throw an exception, you should use “orElseThrow”:

String name = Optional.ofNullable(name)
    .orElseThrow(IllegalArgumentException::new);

// throw NoSuchElementException.class
Optional.ofNullable(name).orElseThrow();



Record classes

A typical situation: API had been written with “none thread safe” methods, and we didn’t know about the magic inside the library. What exactly do I mean? Any variables are mutable by default, which means we can change the value they hold. We don’t have read-only methods for classes inside; we can change them inside any threads:

public class TestObject {
  private String string;
  // public contructor 
  // getters and setters
}


In action:

// create new object
ExecutorService executor = Executors.newFixedThreadPool(10);
var testObject = new UsefulObject();

executor.submit(() -> testObject.setTestString("test_1"));
executor.submit(() -> testObject.setTestString("test_2"));
executor.submit(() -> testObject.setTestString("test_3"));
executor.submit(() -> testObject.setTestString("test_4"));


And what about the result?

?



// the result might be anything!
// test_1 or test_2
// or test_3
System.out.println(testObject.getTestString());


Record classes are special classes that act as transparent carriers for immutable data. They are immutable classes and are implicitly final classes, which means they can’t be extended. Just use a record class instead of a class:

record MainObject(int x) { }


The compiler implicitly provides the constructor, getters, equals & hashCode, and toString.

public final class MainObject {

  private final int x;

  public Rectangle(int x) {
        this.x = x;
  }

  double x() { return this.x; }
  // Implementation of equals() and hashCode(), which specify
  // that two record objects are equal if they
  // are of the same type and contain equal field values.
  public boolean equals...
  public int hashCode...
  // An implementation of toString() that returns a string
  // representation of all the record class's fields,
  // including their names.
  public String toString() {...}
}


If we need to share the class now, we can do it without synchronization.


That’s it for the first part! Thank you, and good luck!

Notes

Java primitives

https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html


Class object
https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html


How we can work with collections after Java 7:
https://docs.oracle.com/javase/8/docs/technotes/guides/collections/overview.html


Java intermediate and terminal operation:
https://javaconceptoftheday.com/java-8-stream-intermediate-and-terminal-operations/