Skip to content

Instantly share code, notes, and snippets.

@stoiandan
Last active June 6, 2023 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stoiandan/38fa2c61bab5677ae68cfd24d54e7dd1 to your computer and use it in GitHub Desktop.
Save stoiandan/38fa2c61bab5677ae68cfd24d54e7dd1 to your computer and use it in GitHub Desktop.

Java Interview Recupitulation Topics

These are good topics to review before an interview as of 21 May 2023:

Access Modifiers

Access Modifiers are the public,private,protected and the implicit package (wich is also the default one). Private allows access only inside the class, package allows access withing the packge and public allows access everywhere. The protected one means that the field is avlailable only for classes that inherit.

Be aware of situations like:

package vehicle;

// no explicit access modifier results in the default (implicit) _package_ access modifier
class Car {
  // ctor is private
  private Car() {
  }
}

class VehicleCreator {
   public static Car create() {
      return new Car() // ERROR Car ctor is private, even known Car is itself package
   }
}

Class and interfaces cannot be private, they are default, by... default. Enums are public by default, also the methods of an interface are public by default, since Java 9 can be private as well, and fields of an interace are public, static, final, by default.

Interfaces

Interfaces are great for OOP because you can implement as many as you want on a class.

Since Java 8, there can be default mehtods:

  
   interface Person {
     String name();
    
    default void greet() {
      System.out.println("Hi there " + name() + "!");
    }
  }


 class John implements Person  {
     // Caution! When implemeting an interface, the method being implemented needs be declared public!
     public String name() {
         return "John";
     } 
 }

These default methods can be overriden, as usual use the @Override annotaiton to make sure you didn't to a mistake, as the annotaiton will cause a compilation error in the case that there is no method being overloaded (i.e. if you, by mistake wrote the wrong name or different method signature):

 interface Person {
   String name();
  
  default void greet() {
    // notice the default method ca make use of other methods in the interface
    System.out.println("Hi there " + name() + "!");
  }
}


class John implements Person  {
   // Caution! When implemeting an interface, the method being implemented needs be declared public!
   public String name() {
       return "John";
   }
   
   // NOW this method will be used instead of the default one
   @Override
   public void greet() {
         System.out.println("Hello " + name() + "!");
   }
 
}

private methods in interfaces cannot be abstract, as they cannot be ovrriden, and therefore, never implemnted, so they must be concrete methods:

 interface AreaCalculator {
    int width();
    int height();
    
    default int rectangle() {
      ....
      // here we can use the subtract method, and we know for sure no one will play with it, or change it
    }
    private int substract(int a, int b) {
       return a - b;
    }
 }

Functional Interfaces

Introduced in Java 8, functional interfaces are those interfaces that only have one abstract method (they can have as many default methods as you like), and thefore can be annotated with @FunctionalInterface and therefore can be used as lambdas:

@FunctionalInterface
public interface Producer<T> {
    T produce();
}

// a dummy lambda expression that takes no arguemnt an reutnrs 6, is a simple way to create a Producer
Producer<Integer> = () -> 6;

Threads

Threads are a mechanish that allows you to run code in parallel. Parellel programming is different from concurent, but this is a technicality.

Concurent is more like:

image

Parallel is:

image

Java is concurent, as threads can share resources more easly.

There are more ways you can run "prallel" code in Java. One methods very common is to implement an interface called Runnable, it's a functional interface that has the run() method:

public static void main(String args[]) {
  Thread t = new Thread(() ->  System.out.println("I'm first!")  );
  Thread t2 = new Thread(() -> { 
        System.out.println("No, I'm first");
        System.out.println("I'm the one who's first");
   });
   
   t.start();
   t2.start();
}

Here the constructor of the Thread class, expects us to pass a runnable, and we can easly do that by simply providing a lambda expression that conforms to runnable (takes no params and returns nothing). Notice, to actually start the two threads, we need call the start method.

The second way to achieve parallel code is to extend the Thread class and call the start method:

class MyThread extends Thread {
  @Override
  public void run() {
  ...
  }
}

new MyThread().start()

This second approach is less common because:

  1. lambdas are more easy two write
  2. if you already extend one class, you cannot extend other ones and this limits you

Syncronization

It's notroious that this type of programming comes with problems such as deadlocks, a situation where two threads each wait for another and therefore, none finishes. To control this there are multiple ways you can ensure part of code that's being runned in parallel, is acutally access sequentially, i.e. by one thread at a time:

  • Synchrnized block
    class Calculator {
    public void calculate(){
       ... // more threads can run this
       synchronized(this){
           ...  // only one thread at a time can run this
       }
       ... more threads can run this
    }

This simply uses the synchronized block that makes use of a so called monitor object, it's a lock, in essence. Once a thead enters it aquaries that lock and only returns it once it exists the sync. block. The idea here is that you can have more blocks that use the same lock or differnet locks depending on what you want to achieve.

You can also have sync. blocks on static methods, Jenkov's tutorial does a great job here.

  • Sync methods

If you've read the preivous paragrah, this will make sense, here you don't need a lock object (or monitor object) you just block on the entire instance method:

      public synchronized void add(int value){
      this.count += value;
  }

What's new in Java

Java 9 introcues modules. In eseence a module just wraps packages, just like packages wrap more classes. But the idea here is not just to create another level of wrapping. Modules can be explicit about what packages they export and, more important, they can speicfy what other modules they depend on. This is trying to solve what Maven/Gradle where trying to solve, and being more explicit about it at the same time.

To create a Java modules, you need a module-info.java file at the root of your newly declared module, and that file looks like:

module com.test.MyModule {
    requires javafx.graphics; // Specifies dependencies
    exports com.test.mymodile.util // Specifies what packages it exprots
}

The var keyword

With some exeptions, like lambdas, you can now just use the var keyword instead of the whole type, and the compiler will infer it for you:

var myString = "Hello I'm still of type String";

Text blocks

Java 14 introduces text blocks:

public String getBlockOfHtml() {
    return """
            <html>

                <body>
                    <span>example text</span>
                </body>
            </html>""";
}

Notice this also take into consideration aligment relative to where the block """ starts. More about this here.

Java 11's Http Client

Java 11 Introduces a new Http Client. More on that here

// Build the client
HttpClient client = HttpClient.newBuilder()
      .version(Version.HTTP_2)
      .followRedirects(Redirect.SAME_PROTOCOL)
      .proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080)))
      .authenticator(Authenticator.getDefault())
      .build();
// build the request
HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://openjdk.org/"))
      .timeout(Duration.ofMinutes(1))
      .header("Content-Type", "application/json")
      .POST(BodyPublishers.ofFile(Paths.get("file.json")))
      .build()

Spring Framework

Spring Framework is compost of a ton of modules providing REST, Secuirity, Databse integration of others. You can thing of the entire Sprinf Framework as a bunch of modules, and you choose what you're interested in, what you need. Sometimes when people say Spring, the can refere to the most popular of all thesse modules, namely the REST/Web part. Recently Spring Boot has becomed a lightweight version of the Web module:

@SpringBootApplication
@RestController
public class DemoApplication {
    public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
    }
    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
      return String.format("Hello %s!", name);
    }
}

This is all you need to get a web server up and running with a hello endpoint responding to the the GET http method.

Spring provides other features like DI (Dependecy Injection) that can automatically inject what you need, usutually this is done via annotations. You declare a component:

@Component("logger")
public class Logger {
    public void log() {
        ...
    }
}

And then you can tell Spring where to instantiate it, without you having to write the code:

public class RestController {
    private Logger logger;
    
    @Autowired
    public RestController(Logger logger) {
        this.logger = logger;
    }
}

That was an example where the dependency is injected directly into the constructor, it can also be used on a field:

public class RestController {
    @Autowired
    private Logger logger;
    
}

The first appraoch, constructor injection is prefered, see here

Spring also provides a JPA implementation, that can map db entities and create repositories, etc.

Miscellaneous

This section is about all sort of quirks of the Java langauge, so called "interview questions".

Strings & String Pool

Strings are an important part of any programming langauge. The JVM has a area specailly alocated to cache strings called the "string pool". To be with with, strings are normally comapred with the equals method, inherited from Object and specifically overriden in the String to compare two strings, letter by letter:

String john = new String("John");
String john2 = new String("John");

System.out.println(john == john2); // false

The previous comparison is false because == comparase memory locations, and since we've used the new operator, it means we've explictly allocated new memory for each of our variables, john and john2.

However, notice that should we write:

String john = "John"
String john2 = "John"

System.out.println(john == john2); // true

This is a different situation. Here we use string litterals, something the Java compiler is able to detect an cache, i.e. it sees the same exact string appear twice, and only allocates memory for it once, making both john and john2 point to the same location.

This special location where strings accumulate in order to be cached (i.e. use the same resource multiple times) is called the string pool.

We can also manually use this stirng pool ourselves by using the intern() method of String. This will, in effect, cause the JVM to check if the string is already in the pool, and if so, use it, otherwise, allocate memory for it and add it to the pool:

String john = new String("John").intern(); // this allocates memory for "John", the caches it
String john2 = john.intern(); // now when intern() check for the string, it's already in the pool

System.out.println(john == john2); // now this is true;

String are immutable.Something is immutable if it doesn't change. Consider

   String john = "John";
   john.concat("Johnson");
   

john.cocant("Johnson"); results in creating an entierly new String and the initial john variable is still only equal to "John" even after the concat operation. If we want to opperate on the new string, we need to save it:

   String john = "John";
   Strinh john2 = john.concat("Johnson");
   

Autoboxing and Unboxing

This has to do with the primitive types that Java has and are not classes: char,int,double,float and their anti-type classes: Character,Integer,Double,Float. Put it simple, auto-boxing, is the process of implictly converting a primitive to it's class corespondent:

   int i = 5;
   Integer classI = i;

In Java, integers from -128 to 127 are cached:

  Integer a = 24;
  Integer b = 24;
  System.out.println(a == b); // true
  
  Integer c = 345;
  Integer d = 345;
    System.out.println(c == d); // false

Unboxing is just the reverse, i.e. transforming the class to a primitive (more info here):

List<Double> ld = new ArrayList<>();
        ld.add(3.1416);    // Π is autoboxed through method invocation.

        // 2. Unboxing through assignment
        double pi = ld.get(0);
        System.out.println("pi = " + pi);

Generics

The idea behind generics is to avoid repeating code, to write more generic code, that can be applied to more data types. Istead of:

   class IntegerList {
   ....
   }
   class DoubleList {
   ....
   }
   class FloatList {
   ...
   }

Notice the overwhelming repetition. With generics we can just:

   class List<T> {
   ...
   }

and then, we can instantiate concrete types, like List<Integer>, List<Double>, List<Float>. However, notice, generics are not only good for containers, like Set<T>,List<T>,Map<K,V>, but also to abstract concepts. Imageine you want to generalise the idea that different animals eat different types of food, but they all have something in common, still. You can use generics on interfaces:

interface Eating<T>  {
     void eat(T food);
     T generateFood();
}

and you have all sorts of animals:

 class Cow {
 ...
 }
 class Horse {
 ...
 }
 class Dog {
 ...
 }

And food:

interface AnimalFood {
}

// note we write extends if an interface implenmets, or extends another interface
// this simply means has all the AnimalFood interfaces requires, plus extra
// but you  CAN NOT convert types:
//        Hay h = null;
 //       AnimalFood f = h; <---- This will NOT work;
interface Hay extends AnimalFood {
 ....
}
interface DogFood extends AnimalFood {
....
}

Well now they can implement the interface:

class Cow implements Eating<Hay> {
    public void eat(Hay h) {
    }
}

We can even be more specific and have uppoer or lower bounds, telling the compiler that we want our generic type T to be a supper or to extend anotehr class. So let's modify that:

interface Eating<T extends AnimalFood >  {
     void eat(T food);
     T generateFood();
}

Note, in generics, even known AnimalFood is an interface and not a class, we still write extends. We don't just have to stop at one constraint, we can have T implemnt multiple interfaces: T extends Foo,Bar where Foo and Bar are interfaces.

Well now, instead of having a traditional approach, where you have an Animal base class for Dog,Cow,Horse you can instead generalise uppon animals that eat different types of food, you can have a List of animals that eat Hay 🐮🐴 List<Eating>, just like that:

        Cow c = new Cow();
        Dog d = new Dog();
       List<Eating> animals =  List.of(c,d);

We can write List<Eating> because of a feature called type erasure. In essence, Java only uses generics at compile time. to check if the code makes sense. But then, when it actually create the bytecode, because of want to provide backwards compatibility, the compiler just changes the code, and does some casting where it needs to and replaces our types with palin old Object, or whatever common denominator it finds. Anyways, with a List<Eating> when run into the problem of how do we use it?:

   
   for(Eating f : foodList) {
      f.eat( /* Now what?! */ );
   }

Ad hoc polymorphism is a potential solution:

   static void eatFood(Hay h) {
    ....
   }
   static void eatFood(DogFood f) {
    ....
   }
   
   ....
   for(Eating f : foodList) {
       eatFood(f);
   }

Notice, this can result in runtime error, if for example there's no suitable method found.

Generics also introduce the idea of wildcard symbol (?). For example this:

List<? extends AnimalFood> lst;

This means that lst can be at runtime List<Hay>,List<DogFood>, or whatever type is compatible, it can even be a List<AnimalFood>:

Hay h = ...
DogFood d = ...
List<? extends AnimalFood> lst = List.of(h,d); // compiler decides to make it a List<AnimalFood> 

And while it's safe to itterate over this list:

List<? extends AnimalFood> lst = ...
for(AnimalFood f : lst) {
   ...
}

or assign it alltogether a compatible type:

List<? extends AnimalFood> lst = ...
List<DogFood> dogF = ...
List <Hay> hayF = ...
List <AnimalFood> anF = ..
lst  = dogF; // ok, it's a supported type
lst  = hayF; // ok, it's a supported type
lst  = anF; // ok, it's a supported type

Because we do not know the exact type the list is holding, even known we are able to cast it to a lowest denominator, like we AnimalFood, like we did earlier, we cannot:

DogFood f = ...
List<? extends AnimalFood> lst = ...
lst.add(f); // COMPILE error

This is because what if lst is of type List<Hay> and we've just added a Dog? That would make no sense. Note, you can also write things List<? super SomeClass>.

Inheritance, Abstract and Sealed Classes

Inhertiance is the idea to write base code that is then inherited, that is, it's accesible to others. Inhertiance is specific to classes, only classes can inherit, once, from anoter class. This limitation to once is because of the dreaded diamon prolbme(i.e. what do you do if you inherit the same piece of information (a field or a method) from two classes?) Inheritance looks like:

class MyClass extends Object {
...
}

A class can inherit the public,protected and default (package) fields and methods. Every class, inherits Object. Methods that are inehrited can be used as such:

class Base {
   void say() {
      System.out.printl("Hi!");
   }
}

class Derived extends Base {
     Derived() {
       say(); // <--- prints "Hi!"
     }
}

Or they can override the functionality. When doing so, it's recommended to use @Overrides for the compiler to fail, in case you do a mistake, like a typo, and don't actually override anything:

class Base {
   void say() {
      System.out.printl("Hi!");
   }
}

class Derived extends Base {
     @Overrides
        void sy() { // <--- typo, method named "sy" instead of "say", compilation will fail
      System.out.printl("Hi!");
   }
     Derived() {
       say(); // <--- prints "Hi!"
     }
}

Going back to the subject, this is how you would override:

class Base {
   void say() {
      System.out.printl("Hi!");
   }
}

class Derived extends Base {
     @Overrides
     public void say() {
     System.out.printl("Hi from Derived!");
     }
     Derived() {
       say(); // <--- prints "Hi!"
     }
}

Notice the overridden method is public in the derived class, where it was packge in the base class. Making an overriden method more accessible is not a problem, but if it would have been public in the base class, you could not make it less accessible in the derived class. If you do something like:

   Base derived = new Derived();
   derived.say(); // <--- this will print "Hi from Derived!"

This is because the type of the variable (aka what's on the left hand side of the = operator) Base derived tells you what methods you can access. In other words, you can only access those methods of the type Base. However, because on the right hand side of the = operator we have new Derived(), we will use those overloads that are specific to the derived class. This touches a more general problem, namely how does Java resolve methods? What Java does it it makes a list of all the methods of the class and the subclass it dervives from, all the chain (for example A -> B -> C, meaing C derives from B wich itself dervies form A) up until it reaches the base class of all classes Object and searches for the best method it can find. For example:

class Base {
   void print(Object d) {
     System.out.println("In base class");
   }
   }
   class Derived extends Base {
      void print(String s) {
          System.out.println("In derived class");
      }
   }
   ...
   new Derived().print(5); <-- will print "In base class", because Integer (see autoboxing) can be cast to Object, but not to String

However, if there is no exat fit and the compiler is able to resolve more than one method, where the options are on the same level of the chain of inheritance, it will fail to compile:

class Base {
   void print(Float f) {
     System.out.println("In base class");
   }
   }
   class Derived extends Base {
      void print(Double d) {
          System.out.println("In derived class");
      }
   }
   ...
   new Derived().print(null); <-- Both Double and Float extend form the same base class, therefor they are on the same level on the inheritance chain, and null can be cast to anything, so wich one do we choose? None! Compiler throws compilation error

You can prevent other classes to dervice from a class, by markign that class as final, a class cannot be both final and abstract at the same time. Since Java 17, the sealed keywowrd was introduced to only permise certain classes to extend a class or implement an interface:

// This interface only permits Car and Truck to implement it.
public sealed interface Service permits Car, Truck {  

Abstract classes are delcared using the abstract modifier, they may or may not actually contain any abstact method, and while they can have constructors, they can never be directly instantiated. If a method is makred as abstract, it must not have a body, and it must be implemented in a derived class that can be instantiated. You can have an abstract class be inherited from another absttact class, and then, the absttract methods don't need to be implemented, but can be.

Exceptions and Error handling

Exceptions and error occure in all programs. You try to open a file, but it's not there, because the user deleted it, or he/she changed permissions, etc. Java provides to types to handle these sittuations, erros and exceptions. These are normal classes that come from a class hierarchy:

image

All error and exception classes inherit from thrwoable class. So you can always use the throw keyword on such a class, interrupt the normal flow of the program, and unwind the stack, i.e. return back up to the main method of a Java programm, and even there, if the error/exception is not catched, exit the program abruptly.

The main difference between errors and exceptions is a conventional one. It's not that you cannot catch erros, you can, since they inherit from throwable, but you should not try catch them! When you raise an errror you want to say "this is a critical condition from wich we cannot recover, and the program should end". On the other hand exceptions represent situations where you can do something about the problem. Or even if you can't, it's not that critical.

Exceptions

The difference between checked and runtime exceptions is that checked ones need always be handeled. Either with catch block or by marking a method as throws in wich case, it will be handeled by the caller.

 int divide(int a, int b) throws ArithmeticException  {
 
    if (b == 0) {
     throw new ArithmeticException("Cannot divide by 0");
    }
 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment