Thread See Here
Create a thread
Generally, we can create a thread in two ways: Implement Runnable
interface or extend Thread
class.
But we use Runnable interface instead of Thread class, because:
-
Thread
implements
runnable, when extending Thread, we only override run() method. this is a violation ofIS-A
principle. -
After extending
Thread class, we can’t extend other classes.
Example:
Runnable task = () -> someReallyLongProcess();
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(task);
We need to access the result and when bug happens, we need to know the message of the exception. But Runnable
can not return the result
and throw exception
, so we can use Callable
and Future
to solve this problem.
Callable and Future
Callable
Callable
is a functional interface, which has only one method call(). Callable is similar to Runnable, but Callable can return the result and throw exception.
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Executor
only accepts Runnable
, so we need to use ExecutorService
to run the Callable
thread.
public interface ExecutorService extends Executor {
// Submits a value-returning task for execution and returns a Future representing the pending results of the task.
<T> Future<T> submit(Callable<T> task);
// Submits a Runnable task for execution and returns a Future representing that task.
Future<?> submit(Runnable task);
// Submits a Runnable task for execution and returns a Future representing that task.
<T> Future<T> submit(Runnable task, T result);
}
Future
The Future
represents the result of Thread. When thread executed, the result or the exception message will be stored in the Future for main thread to access.
// In the main thread
Callable<String> task = () -> buildPatientReport();
Future<String> future = executor.submit(task);
String result = future.get(); // blocking here until finished to get the result
DownSide
: Threads should be independent, also we need multi-threads to improve the performance, but Future
will block
the main thread, so we need to use CompletableFuture to solve this problem.
CompletableFuture
CompletableFuture
is almost the same as a Future object with more methods and more capabilities. (runnable
, supplier
)
Runnable task = () -> System.out.println("Hello world");
ExecutorService service = Executors.newSingleThreadExecutor();
Future<?> future = service.submit(task); // use future object
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(task);
CompletableFuture
do not support Callable
, only support Runnable
and Supplier
.
Supplier<String> task = () -> readPage("https://google.com");
ExecutorService service = Executors.newSingleThreadExecutor();
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(task);
Future chain to handle the results:
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(task);
CompletableFuture<Integer> lengthFuture = completableFuture.thenApply(String::length).thenApply(len -> len * 2).exceptionally(ex -> 0);
ExecutorService
ExecutorService
is a thread pool used to manage threads, and can submit tasks to the thread pool for execution. Generally, we can use ExecutorService
to run Runnable, Callable, CompletableFuture threads.
Advantage:
-
easy to manage threads and reuse threads, which can reduce the overhead of thread creation and improve performance.
-
can limit the number of threads, avoid too many threads to cause system crash.
Disadvantage:
-
a careless threads pool size can halt the system and bring performance down
-
Always shut down the executor service after tasks are completed and service is no lon..ger needed. Otherwise, JVM will never terminate.
OOP
Object-oriented programming aims to implement real-world entities, the main aim is to bind together the data and functions that operate on them so that no other part of the code can access this data except that function.
Inheritance
-
Inheritance Derive something specific from something generic, a class can inherit all properties and methods of another class and add its own modification
Use Extend keywords to declare inheritance, inheritance in Java implies that there is an IS-A relationship between the classes.
Type: single, multiple, multilevel, hierarchical, hybrid. Java doesn’t support Multiple inheritance
-
Diamond Problem When two classes inherit from the same class, and a third class inherits from both the two classes, the third class will have two copies of all the data members of the base class, which leads to ambiguity.
Java solves this problem by using
- interfaces: which is a collection of abstract methods and static constants.
- aggregation: We can also use aggregation to include objects of multiple classes within its own implementation. This allows the class to access the methods and properties of the aggregated objects without directly inheriting from them, thus avoiding the conflicts and ambiguities of multiple inheritance.
Encapsulation
-
Encapsulation is a mechanism of wrapping the data (variables) and code acting on the data (methods) together as a single unit. In encapsulation, the variables of a class will be hidden from other classes, and can be accessed only through the methods of their current class. Therefore, it is also known as data hiding.
To achieve encapsulation in Java:
- Declare the variables of a class as private.
- Provide public setter and getter methods to modify and view the variables values.
Example: reflection in Java
Polymorphism
-
Polymorphism means the ability to take more than one form. In Java, polymorphism allows us to access an object in multiple ways, such as:
- Method overloading
- Method overriding
- Operator overloading
Method overloading is a feature that allows a class to have more than one method having the same name, if their argument lists are different. It is similar to constructor overloading in Java, that allows a class to have more than one constructor having different argument lists.
Method overriding means to override a method in a subclass. It is used to provide the specific implementation of a method which is already provided by its superclass.
Operator overloading is a feature that allows Java programmers to redefine the implementation of an operator (such as +, -, *, /, %) for user-defined classes.
Abstraction
-
Abstraction is a process of hiding the implementation details and showing only functionality to the user. Another way, it shows only essential things to the user and hides the internal details. It is also used at high level design phase where the actual software is not available.
In Java, abstraction is achieved by interfaces and abstract classes. We can achieve 100% abstraction using interfaces.
Difference between interface and abstract class:
Abstract class in Java is a class which is declared using abstract keyword. It can have abstract and non-abstract methods (method with the body).
Interface in Java is a blueprint of a class. It has static constants and abstract methods only. (after Java 8, it can have default and static methods also). All the methods of interfaces are public and abstract by default. We can’t create instance of interfaces.
Solid Principle
Single Responsibility Principle
-
A class should have one and only one reason to change, meaning that a class should have only one job.
Example:
Suppose you have a shape class, like triangle, you want to have a method to give you the area of the shape, instead of writing a method called calculateArea, you should create a new class called areaCalculator which is responsible for calculating area of all types, and call areaCalculator when you need to calculate the area.
Open-Closed Principle
-
Objects or entities should be open for extension but closed for modification.
Example:
you have different payment methods, like card, cash, third parties, instead of checking input several times with if else statement, you should generate a new interface like payProcessor to handle the payment, this is good for decoupling. You don’t have to modify this class when a new payment method is added, all you need to do is generate a new class and extend the payProcessor.
Liskov Substitution Principle
-
Every subclass/derived class should be substitutable for their base/parent class.
Example:
List and arrayList are both list, so you can use arrayList anywhere you use list.
Interface Segregation Principle
-
A client should never be forced to implement an interface that it doesn’t use
Example:
Take payment as mentioned above as an example, A and B both implement payProcessor, A have charge and refund function, suppose B do not support refund, but B have to implement an empty method, this is a violation. Instead, we should separate the parProcessor to chargeProcessor, refundProcessor.
Dependency Inversion Principle
-
The high-level module must not depend on the low-level module, but they should depend on abstractions.
Example:
Still use payment as an example, if you want to use a new payment method, you should not modify the payProcessor, instead, you should create a new class and extend the payProcessor, and use the new class to handle the new payment method.
Final, Finally, Finalize
-
final
is a keyword to declare a constant variable, a method or a class.- final variable: value (reference) cannot be changed once assigned.
- final method: cannot be overridden.
- final class: cannot be extended by other class
-
finally
is used at the end of the try-catch block to execute the code within it regardless of the result of the try-catch block. -
finalize
is a method of Object class that is called by the garbage collector when the object is no longer referenced. Use try with resource instead, because finalize() calling is unpredictable, sometimes will cause GC pause.
Immutable Class
Definition: Immutable Class is a class whose instances (objects) cannot be modified after they are created.
Example: Wrapper classes, String (Security reason, hashcode)
How to create Immutable Class
-
Declare the class as final so it can’t be extended.
-
Make all fields private and final.
-
No setter methods. All variables are set in the constructor with the deep copy of the input.
-
Return copies of feild data in getter methods.
Why String is immutable in Java?
-
Security concern
: sensitive information such as password, network connection, etc. Being immutable prevents malicious code from modifuing sensitive information. -
Thread safe
: Immutable objects are by default thread safe, can be shared among multiple threads. -
Hashcode
: String is widely used as key in Map and other collection classes. Being immutable guarantees that hashcode will always be same so that it can be cached without worrying about the changes. -
String Pool
: Save the runtime memory by reusing strings.Java uses a string pool to store and reuse string literals. When you create a string, Java checks if it already exists in the string pool. If it does, the existing instance is returned, if not, a new instance is created.
String, StringBuffer, StringBuilder
They both used to store string values
-
String
is immutable, used to store values with fixed size. (Security reason, Thread safe, Hashcode, String Pool) -
StringBuffer
used when we do not need to consider the thread safety, and when we need to frequently modify the content of the string. -
StringBuilder
used in multi-threaded environment, when we need to frequently modify the content of the string.