These are good topics to review before an interview as of 21 May 2023:
- Access Modifiers
- Inheritance, Abstract and Sealed classes
- Interfaces
- Parallel and concurent programming (aka threads)
- What's new in Java (> Java 8)
- Spring Framework
- Exceptions and Error handling
- Miscellaneous
- Generics
- Record types
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 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;
}
}
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 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:
Parallel is:
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:
- lambdas are more easy two write
- if you already extend one class, you cannot extend other ones and this limits you
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;
}
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
}
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";
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 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 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.
This section is about all sort of quirks of the Java langauge, so called "interview questions".
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");
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);
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>
.
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 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:
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.
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");
}
}