G.L.
BAJAJ INSTITUTE OF TECHNOLOGY & MANAGEMENT
[Link] 4th Semester — CSE-DS/CSE/AI/AIML/CSIT
OOPS with Java (BCS403)
Pre University Test (Even Sem 2025-26) — Complete Solution
SECTION A — Short Answer Questions (2 Marks Each)
Q1(a) Class and Object in Java
A class is a blueprint or template that defines the properties (fields) and behaviors (methods) of objects.
An object is a real-world instance of a class that occupies memory.
Example: Class
class Student {
int rollNo;
String name;
void display() {
[Link]("Roll No: " + rollNo + ", Name: " + name);
}
}
Example: Object
public class Main {
public static void main(String[] args) {
Student s = new Student(); // Object creation
[Link] = 101;
[Link] = "Amit";
[Link]();
}
}
Output: Roll No: 101, Name: Amit
Q1(b) Interfaces and Multiple Inheritance in Java
Java does not allow multiple inheritance through classes to avoid the diamond problem. However, a class
can implement multiple interfaces, thereby achieving multiple inheritance of behavior.
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
public void fly() { [Link]("Duck flies"); }
public void swim() { [Link]("Duck swims"); }
}
Here Duck implements both Flyable and Swimmable, achieving multiple inheritance through interfaces.
Q1(c) Static Import in Java Packages
Static import allows you to access static members (fields and methods) of a class directly without using
the class name prefix. It reduces verbosity in code.
import static [Link];
import static [Link];
public class CircleArea {
public static void main(String[] args) {
double area = PI * 5 * 5; // No need for [Link]
[Link](sqrt(25)); // No need for [Link]()
}
}
Q1(d) Major States in Thread Life Cycle
A thread in Java goes through the following major states:
• New — Thread object created but start() not called yet
• Runnable — start() called; thread is ready to run or running
• Blocked — Thread waiting to acquire a lock
• Waiting — Thread waiting indefinitely for another thread (wait())
• Timed Waiting — Thread waiting for a specific time (sleep(), join())
• Terminated (Dead) — Thread has finished execution
Q1(e) Try-With-Resources in Java
Try-with-resources is a feature (Java 7+) that automatically closes resources (like files, streams) after the
try block, eliminating the need for explicit finally blocks.
import [Link].*;
public class TryWithRes {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("[Link]"))) {
String line = [Link]();
[Link](line);
} catch (IOException e) {
[Link]([Link]());
}
// br is automatically closed here
}
}
Q1(f) HashMap — Create and Insert
Syntax to create a HashMap and insert a key-value pair:
import [Link];
public class HashMapDemo {
public static void main(String[] args) {
// Create a HashMap
HashMap<String, Integer> map = new HashMap<>();
// Insert key-value pairs
[Link]("Alice", 90);
[Link]("Bob", 85);
[Link]("Charlie", 95);
[Link](map);
}
}
Q1(g) Bean Scopes in Spring Framework
Spring provides the following bean scopes:
• singleton — (Default) Only one instance per Spring container
• prototype — New instance created every time the bean is requested
• request — One instance per HTTP request (Web applications)
• session — One instance per HTTP session (Web applications)
• application — One instance per ServletContext lifecycle
• websocket — One instance per WebSocket lifecycle
@Bean
@Scope("prototype")
public MyBean myBean() {
return new MyBean();
}
SECTION B — Descriptive Questions (7 Marks Each)
Q2(a) Abstraction and Abstract Classes in Java [7 Marks]
Abstraction
Abstraction is the process of hiding implementation details and showing only the essential features of an
object to the user. It focuses on 'what' an object does rather than 'how' it does it. In Java, abstraction is
achieved using abstract classes and interfaces.
Abstract Class
An abstract class is a class declared with the abstract keyword. It can have both abstract methods
(without body) and concrete methods (with body). An abstract class cannot be instantiated directly.
Abstract Method
An abstract method is a method without a body, declared using the abstract keyword. Any subclass that
extends an abstract class must provide the implementation of all abstract methods, otherwise the
subclass must also be declared abstract.
Key Features:
• Declared using abstract keyword
• Can have constructors, instance variables, concrete methods
• Cannot be instantiated (new AbstractClass() is not allowed)
• Subclass must override all abstract methods
• Can have 0 to N abstract methods
Example:
abstract class Shape {
String color;
// Constructor
Shape(String color) {
[Link] = color;
}
// Abstract method — no body
abstract double area();
// Concrete method
void display() {
[Link]("Color: " + color + ", Area: " + area());
}
}
class Circle extends Shape {
double radius;
Circle(String color, double radius) {
super(color);
[Link] = radius;
}
@Override
double area() {
return [Link] * radius * radius;
}
}
class Rectangle extends Shape {
double length, width;
Rectangle(String color, double l, double w) {
super(color);
[Link] = l; [Link] = w;
}
@Override
double area() {
return length * width;
}
}
public class Main {
public static void main(String[] args) {
Shape c = new Circle("Red", 5.0);
Shape r = new Rectangle("Blue", 4.0, 6.0);
[Link](); // Color: Red, Area: 78.53
[Link](); // Color: Blue, Area: 24.0
}
}
Q2(b) Creating Threads in Java [7 Marks]
Method 1: Extending Thread Class
class MyThread extends Thread {
public void run() {
[Link]("Thread running: " + [Link]().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
[Link]();
}
}
Method 2: Implementing Runnable Interface
class MyRunnable implements Runnable {
public void run() {
[Link]("Runnable Thread: " + [Link]().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
[Link]();
}
}
Method 3: Using Lambda Expression (Java 8+)
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> [Link]("Lambda Thread"));
[Link]();
}
}
Which Method is Better?
Implementing Runnable interface is more suitable because:
• Java supports single inheritance; extending Thread blocks inheriting other classes
• Runnable separates the task (run()) from thread management
• Runnable objects can be reused with different Thread instances
• Better compatibility with thread pools (ExecutorService)
Q2(c) Functional Interfaces in Java [7 Marks]
What is a Functional Interface?
A functional interface is an interface that contains exactly one abstract method. It can have multiple
default or static methods. Functional interfaces are the foundation of lambda expressions in Java 8. They
are annotated with @FunctionalInterface.
Examples of Built-in Functional Interfaces:
• Runnable — void run()
• Callable<V> — V call()
• Comparator<T> — int compare(T o1, T o2)
• Predicate<T> — boolean test(T t)
• Function<T,R> — R apply(T t)
Way 1: Implementing using Anonymous Inner Class
@FunctionalInterface
interface Greeting {
void greet(String name);
}
public class Main {
public static void main(String[] args) {
// Anonymous inner class
Greeting g = new Greeting() {
public void greet(String name) {
[Link]("Hello, " + name);
}
};
[Link]("Alice");
}
}
Way 2: Implementing using Lambda Expression
public class Main {
public static void main(String[] args) {
// Lambda expression — concise and clean
Greeting g = name -> [Link]("Hello, " + name);
[Link]("Bob");
}
}
Lambda expressions provide a cleaner and more readable way to implement functional interfaces
compared to anonymous inner classes.
Q2(d) Collection Framework Hierarchy and ArrayList vs LinkedList [7 Marks]
Java Collection Framework Hierarchy:
[Link]
└── Collection (Interface)
├── List (Interface)
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector
├── Set (Interface)
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet (SortedSet)
└── Queue (Interface)
├── LinkedList
├── PriorityQueue
└── Deque --> ArrayDeque
Map (Interface — separate hierarchy)
├── HashMap
├── LinkedHashMap
└── TreeMap (SortedMap)
ArrayList vs LinkedList:
Feature ArrayList LinkedList
Data Structure Dynamic Array Doubly Linked List
Access Time O(1) — Random access fast O(n) — Sequential access
Insertion/Deletion O(n) — Slow (shifting needed) O(1) — Fast at head/tail
Memory Less — only data More — data + two pointers
Use Case Frequent read operations Frequent insert/delete
Implements List interface only List, Deque, Queue
Q2(e) RESTful Web Services with GET, POST, PUT, DELETE [7 Marks]
What is RESTful Web Service?
REST (Representational State Transfer) is an architectural style for designing networked applications.
RESTful web services use HTTP methods to perform CRUD operations on resources identified by URLs.
HTTP Methods in REST:
• GET — Retrieve resource data (Read operation, idempotent)
• POST — Create a new resource (Write operation, not idempotent)
• PUT — Update an existing resource completely (idempotent)
• DELETE — Remove a resource (idempotent)
Spring Boot REST Controller Example:
@RestController
@RequestMapping("/students")
public class StudentController {
private List<Student> students = new ArrayList<>();
// GET — Retrieve all students
@GetMapping
public List<Student> getAllStudents() {
return students;
}
// GET by ID
@GetMapping("/{id}")
public Student getById(@PathVariable int id) {
return [Link](id);
}
// POST — Add new student
@PostMapping
public Student addStudent(@RequestBody Student s) {
[Link](s);
return s;
}
// PUT — Update existing student
@PutMapping("/{id}")
public Student update(@PathVariable int id, @RequestBody Student s) {
[Link](id, s);
return s;
}
// DELETE — Remove student
@DeleteMapping("/{id}")
public String delete(@PathVariable int id) {
[Link](id);
return "Deleted successfully";
}
}
SECTION C — (Attempt Any One from Each Pair) [7 Marks Each]
Q3(a) Method Overloading vs Method Overriding [7 Marks]
Feature Method Overloading Method Overriding
Definition Same method name, different Same method name and parameters
parameters in same class in parent and child class
Class Same class Parent and child class
Polymorphism Compile-time (static) Runtime (dynamic)
Return Type Can be different Must be same or covariant
Access Modifier Can change freely Cannot reduce visibility
static methods Can be overloaded Cannot be overridden
Binding Early binding Late binding
Method Overloading Example:
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
public static void main(String[] args) {
Calculator c = new Calculator();
[Link]([Link](2, 3)); // 5
[Link]([Link](2.5, 3.5)); // 6.0
[Link]([Link](1, 2, 3)); // 6
}
}
Method Overriding Example:
class Animal {
void sound() {
[Link]("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
[Link]("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog(); // Runtime polymorphism
[Link](); // Output: Dog barks
}
}
Q3(b) Types of Constructors in Java [7 Marks]
What is a Constructor?
A constructor is a special method that is automatically called when an object is created. It has the same
name as the class and no return type. Constructors are used to initialize object fields.
1. Default Constructor (No-Argument Constructor):
A constructor with no parameters. Java provides one automatically if no constructor is defined.
class Box {
int length, width;
Box() { // Default constructor
length = 10;
width = 5;
}
public static void main(String[] args) {
Box b = new Box();
[Link]("Length: " + [Link] + ", Width: " + [Link]);
}
}
2. Parameterized Constructor:
A constructor that accepts arguments to initialize fields with custom values.
class Student {
int id; String name;
Student(int id, String name) { // Parameterized constructor
[Link] = id;
[Link] = name;
}
public static void main(String[] args) {
Student s = new Student(101, "Ravi");
[Link]([Link] + " " + [Link]);
}
}
3. Copy Constructor:
A constructor that creates a new object as a copy of an existing object.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
Point(Point p) { // Copy constructor
this.x = p.x;
this.y = p.y;
}
public static void main(String[] args) {
Point p1 = new Point(3, 4);
Point p2 = new Point(p1); // Copy of p1
[Link](p2.x + ", " + p2.y); // 3, 4
}
}
Constructor Chaining using this():
class Employee {
int id; String name; double salary;
Employee() { this(0, "Unknown", 0.0); }
Employee(int id, String name, double salary) {
[Link] = id; [Link] = name; [Link] = salary;
}
}
Q4(a) Reading and Writing Files using Byte Streams [7 Marks]
Byte Streams in Java:
Byte streams handle I/O of raw binary data. The main classes are FileInputStream (reading) and
FileOutputStream (writing). They read/write data byte by byte.
Writing to a File using FileOutputStream:
import [Link].*;
public class FileWriteDemo {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("[Link]")) {
String content = "Hello, Java File Handling!";
byte[] bytes = [Link]();
[Link](bytes);
[Link]("Data written successfully.");
} catch (IOException e) {
[Link]();
}
}
}
Reading from a File using FileInputStream:
import [Link].*;
public class FileReadDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("[Link]")) {
int byteData;
[Link]("File Content: ");
while ((byteData = [Link]()) != -1) {
[Link]((char) byteData);
}
} catch (IOException e) {
[Link]();
}
}
}
Reading all bytes at once (efficient):
try (FileInputStream fis = new FileInputStream("[Link]")) {
byte[] allBytes = [Link]();
[Link](new String(allBytes));
}
Note: FileInputStream and FileOutputStream work with binary data. For text files,
BufferedReader/PrintWriter with character encoding is preferred.
Q4(b) Inter-Thread Communication using wait() and notify() [7 Marks]
Concept:
Inter-thread communication allows threads to coordinate with each other. The wait() method causes the
current thread to release the lock and wait, while notify() wakes up a waiting thread. These methods are
defined in the Object class and must be called inside synchronized blocks.
Key Methods:
• wait() — Causes current thread to wait until notify() or notifyAll() is called
• notify() — Wakes up one thread that is waiting on the object's monitor
• notifyAll() — Wakes up all threads waiting on the object's monitor
Producer-Consumer Example:
class SharedResource {
int data;
boolean produced = false;
synchronized void produce(int val) throws InterruptedException {
while (produced) wait(); // Wait if already produced
data = val;
produced = true;
[Link]("Produced: " + data);
notify(); // Notify consumer
}
synchronized void consume() throws InterruptedException {
while (!produced) wait(); // Wait if nothing produced
[Link]("Consumed: " + data);
produced = false;
notify(); // Notify producer
}
}
public class InterThreadDemo {
public static void main(String[] args) {
SharedResource res = new SharedResource();
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) [Link](i);
} catch (InterruptedException e) { [Link](); }
});
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) [Link]();
} catch (InterruptedException e) { [Link](); }
});
[Link]();
[Link]();
}
}
// Output:
// Produced: 1
// Consumed: 1
// Produced: 2
// Consumed: 2
Q5(a) Java Stream API — Intermediate & Terminal Operations [7 Marks]
What is Java Stream API?
Java Stream API ([Link]) introduced in Java 8 allows functional-style processing of collections.
Streams do not store data; they process data from a source (collections, arrays) in a pipeline of
operations.
Stream Pipeline Structure:
Source (Collection/Array) --> Intermediate Operations --> Terminal Operation
Intermediate Operations (Lazy — return a Stream):
• filter(Predicate) — Filters elements based on condition
• map(Function) — Transforms each element
• sorted() — Sorts elements
• distinct() — Removes duplicates
• limit(n) — Limits to first n elements
• skip(n) — Skips first n elements
Terminal Operations (Eager — trigger processing):
• collect([Link]()) — Collects results into a list
• forEach(Consumer) — Performs action for each element
• count() — Returns count of elements
• reduce() — Reduces stream to a single value
• findFirst() — Returns first element
• anyMatch() / allMatch() — Boolean matching
Example with Intermediate and Terminal Operations:
import [Link].*;
import [Link].*;
public class StreamDemo {
public static void main(String[] args) {
List<String> names = [Link]("Alice", "Bob", "Anna", "Charlie", "Amy");
// Filter names starting with 'A', sort, and collect
List<String> result = [Link]()
.filter(n -> [Link]("A")) // intermediate
.sorted() // intermediate
.collect([Link]()); // terminal
[Link](result); // [Alice, Amy, Anna]
}
}
Program: Sum of Even Numbers from 1 to 10 using ArrayList:
import [Link].*;
import [Link].*;
public class SumEvenNumbers {
public static void main(String[] args) {
// Create ArrayList with integers 1 to 10
ArrayList<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 10; i++) [Link](i);
// Stream pipeline: filter even numbers and sum them
int sumOfEvens = [Link]()
.filter(n -> n % 2 == 0) // Filter: 2,4,6,8,10
.mapToInt(Integer::intValue) // Convert to IntStream
.sum(); // Terminal: sum
[Link]("Sum of even numbers: " + sumOfEvens);
// Output: Sum of even numbers: 30
}
}
Q5(b) switch-case vs switch-expressions in Java [7 Marks]
Traditional switch-case Statement:
The traditional switch statement is a control flow statement used to select one branch from multiple
alternatives. It uses break to prevent fall-through.
public class SwitchCaseDemo {
public static void main(String[] args) {
int day = 3;
String dayName;
switch (day) {
case 1: dayName = "Monday"; break;
case 2: dayName = "Tuesday"; break;
case 3: dayName = "Wednesday"; break;
default: dayName = "Unknown"; break;
}
[Link](dayName); // Wednesday
}
}
Switch Expression (Java 14+):
Switch expressions are more concise, return values directly, and prevent fall-through by using arrow (-
>). No break needed.
public class SwitchExprDemo {
public static void main(String[] args) {
int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Unknown";
};
[Link](dayName); // Wednesday
}
}
Comparison Table:
Feature switch-case switch-expression
Syntax Uses case: and break; Uses case -> (arrow syntax)
Return Value Cannot return value directly Can return value directly
Fall-through Yes (if break missing) No fall-through with ->
Introduced Since Java 1.0 Java 14 (standard)
Verbosity More verbose Concise and cleaner
yield keyword Not applicable Used for multi-line cases
Q6(a) Sorting in Java using Comparable and Comparator [7 Marks]
Comparable Interface:
Comparable is used to define the natural ordering of objects. The class itself implements Comparable
and overrides compareTo() method. It provides a single sorting sequence.
import [Link].*;
class Student implements Comparable<Student> {
int roll; String name; double marks;
Student(int roll, String name, double marks) {
[Link] = roll; [Link] = name; [Link] = marks;
}
@Override
public int compareTo(Student other) {
return [Link]([Link], [Link]); // Sort by marks
}
public String toString() { return name + ": " + marks; }
}
public class ComparableDemo {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
[Link](new Student(1, "Ravi", 85.5));
[Link](new Student(2, "Amit", 92.0));
[Link](new Student(3, "Priya", 78.0));
[Link](list); // Uses compareTo()
[Link](list); // Sorted by marks
}
}
Comparator Interface:
Comparator is used to define external/custom ordering without modifying the class. It allows multiple
sorting sequences by creating different Comparator objects.
import [Link].*;
public class ComparatorDemo {
public static void main(String[] args) {
List<Student> list = [Link](
new Student(1, "Ravi", 85.5),
new Student(2, "Amit", 92.0),
new Student(3, "Priya", 78.0)
);
// Sort by name using Comparator
Comparator<Student> byName = (s1, s2) -> [Link]([Link]);
[Link](byName);
[Link]("By Name: " + list);
// Sort by marks descending
[Link]([Link]((Student s) -> [Link]).reversed());
[Link]("By Marks (desc): " + list);
}
}
Comparable vs Comparator:
• Comparable — modifies class itself; single sort logic
• Comparator — external class; multiple sort logics; more flexible
Q6(b) HashMap in Java — Five Key Methods [7 Marks]
HashMap Overview:
HashMap is a part of Java Collection Framework ([Link]). It stores data as key-value pairs.
It allows one null key and multiple null values. It is not synchronized (not thread-safe). It uses hashing to
store and retrieve elements in O(1) average time.
Internal Working:
• Uses an array of buckets (default capacity 16)
• hash(key) determines bucket index
• Collision handled by Linked List / Red-Black Tree (Java 8+)
Example with Five Methods:
import [Link].*;
public class HashMapDemo {
public static void main(String[] args) {
HashMap<String, Integer> scores = new HashMap<>();
// 1. put(key, value) — Insert/update key-value pair
[Link]("Alice", 90);
[Link]("Bob", 85);
[Link]("Charlie", 92);
[Link]("Diana", 88);
[Link]("After put: " + scores);
// 2. get(key) — Retrieve value by key
[Link]("Alice score: " + [Link]("Alice")); // 90
// 3. containsKey(key) — Check if key exists
[Link]("Has Bob? " + [Link]("Bob")); // true
// 4. remove(key) — Remove entry by key
[Link]("Charlie");
[Link]("After remove: " + scores);
// 5. getOrDefault(key, default) — Get value or default if missing
int score = [Link]("Zara", 0);
[Link]("Zara score: " + score); // 0
// Bonus: entrySet() — Iterate all key-value pairs
for ([Link]<String, Integer> entry : [Link]()) {
[Link]([Link]() + " => " + [Link]());
}
}
}
Other Useful Methods:
• size() — Returns number of key-value pairs
• keySet() — Returns all keys as Set
• values() — Returns all values as Collection
• putIfAbsent(key, value) — Insert only if key not present
• clear() — Removes all mappings
Q7(a) Spring IoC Container and Spring Bean Life Cycle [7 Marks]
Spring IoC (Inversion of Control) Container:
IoC Container is the core of the Spring Framework. It is responsible for instantiating, configuring, and
assembling beans. The control of object creation is inverted — instead of objects creating dependencies
themselves, the Spring container injects them.
Types of Spring IoC Containers:
• BeanFactory — Basic container, lazy loading, lightweight
• ApplicationContext — Advanced container (extends BeanFactory), eager loading, supports
AOP, events, i18n
ApplicationContext context =
new ClassPathXmlApplicationContext("[Link]");
// OR
ApplicationContext context =
new AnnotationConfigApplicationContext([Link]);
Spring Bean Life Cycle:
A Spring bean goes through a well-defined life cycle managed by the IoC container:
1. Bean Definition Loading
|
2. Bean Instantiation (Constructor called)
|
3. Dependency Injection (Properties/fields set)
|
4. [Link]()
|
5. [Link]()
|
6. [Link]()
|
7. [Link]()
|
8. @PostConstruct / [Link]() / init-method
|
9. Bean is READY to use
|
10. [Link]()
|
11. @PreDestroy / [Link]() / destroy-method
|
12. Bean Destroyed
Example — Custom Init and Destroy:
@Component
public class MyBean {
@PostConstruct
public void init() {
[Link]("Bean initialized!");
}
@PreDestroy
public void destroy() {
[Link]("Bean destroyed!");
}
}
Q7(b) Dependency Injection (DI) vs Inversion of Control (IoC) in Spring [7 Marks]
Inversion of Control (IoC):
IoC is a design principle where the control of creating and managing objects is transferred from the
application code to a container or framework. Instead of the programmer writing code to create objects,
the Spring container does it automatically.
// WITHOUT IoC — programmer controls object creation
public class OrderService {
PaymentService ps = new PaymentService(); // Tight coupling
}
// WITH IoC — container controls object creation
@Component
public class OrderService {
@Autowired
PaymentService ps; // Spring injects it — Loose coupling
}
Dependency Injection (DI):
DI is the implementation mechanism of IoC. It is the process of providing dependencies (objects that a
class needs) from outside rather than creating them inside the class. Spring supports three types of DI:
1. Constructor Injection:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
[Link] = paymentService;
}
}
2. Setter Injection:
@Component
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService ps) {
[Link] = ps;
}
}
3. Field Injection:
@Component
public class OrderService {
@Autowired
private PaymentService paymentService; // Spring injects directly
}
Difference — IoC vs DI:
Aspect IoC Dependency Injection
Definition Design principle — transfer object Implementation of IoC — inject
control dependencies
Level High-level concept Concrete technique
Relationship IoC is the principle DI is a way to achieve IoC
Scope Broader concept Specific mechanism
Benefit Loose coupling Easier testing, flexibility