어노테이션은 메타데이터로서 컴파일 및 실행 과정에서 코드가 어떻게 처리될지를 컴파일러나 런타임에 알려주는 역할을 한다. 주로 @AnnotationName 형식으로 작성된다.
어노테이션을 직접 정의할 때는 다음과 같은 형식을 사용한다:
@AnnotationName
public @interface Annotation {
타입 elementName() [default 값];
}
String, Enum, Class, 배열 타입()를 붙여 작성default 키워드를 사용하여 기본값을 설정할 수 있음@Target어노테이션이 적용될 대상을 지정할 때 @Target 어노테이션을 사용한다. @Target의 value 요소는 ElementType 배열로, 여러 대상에 적용 가능하다.
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName {
}
실제 프로젝트에서는 스프링이나 롬복과 같은 라이브러리에서 제공하는 어노테이션을 주로 활용하며, 커스텀 어노테이션을 작성하는 경우는 상대적으로 적다.
어노테이션은 메타데이터를 통해 코드의 컴파일 및 실행 과정을 제어하며, 정의된 타입과 적용 대상을 바탕으로 다양한 곳에 적용 가능하다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
// 어노테이션 형태로 사용할 것이라 @를 붙혀준다.
public @interface AnnotationName {
String elementNameOne(); // 구현부가 없는 메서드로 정의
int elementNameTwo() default 5; // 아무런 값도 설정하지 않았을 때는 5라는 값이 들어가도록 default 값 설정
}@AnnotationName(elementNameOne = "값", elementNameTwo = 40)
public class ClassName {
@AnnotationName(elementNameOne = "값")
int field;
int getField(){
return field;
}
void setField(int field){
this.field = field;
}
@AnnotationName(elementNameOne = "값") // AnnotationName.java의 target에서 메서드 부분을 지우게 된다면 에러 발생
void method(){
}
}상속은 일반 클래스, 추상 클래스, 인터페이스에서 모두 적용 가능한 개념으로, 부모 클래스의 멤버(필드, 메서드)를 자식 클래스가 물려받아 사용할 수 있게 한다. 상속을 통해 코드 재사용성과 기능 확장이 용이해진다.
public class 자식클래스명 extends 부모클래스명 {
// 자식 클래스의 내용
}
부모 클래스 InheritA에서 정의된 필드와 메서드를 자식 클래스 InheritB에서 그대로 사용하거나, 추가 필드 및 메서드를 확장할 수 있다.
// 부모 클래스
public class InheritA {
protected int field1; // 자식 클래스에서 접근 가능
protected void method1() {
System.out.println("InheritA.method1 field1: " + field1);
}
}
// 자식 클래스
public class InheritB extends InheritA {
int field2;
void method2() {
System.out.println("InheritB.method2 field2: " + field2);
}
}
// 상속 사용 예시
public class InheritanceExample {
public static void main(String[] args) {
InheritB inheritB = new InheritB();
inheritB.field1 = 10; // InheritA로부터 상속받은 필드
inheritB.method1(); // InheritA로부터 상속받은 메서드
inheritB.field2 = 30; // InheritB에서 추가한 필드
inheritB.method2(); // InheritB에서 추가한 메서드
}
}
private 필드 및 메서드는 상속되지 않는다.default 접근 제한자는 상속되지 않는다.protected는 자식 클래스에서 접근 가능하도록 상속된다.부모 클래스에서 공통적으로 사용하는 메서드와 필드를 정의하고, 자식 클래스에서 이를 확장하여 새로운 기능을 추가하거나 재정의할 수 있다.
public class Animal {
String name;
void setName(String name) {
this.name = name;
}
void sleep() {
System.out.println(this.name + " Zzz...");
}
}
자식 객체를 생성할 때, 부모 클래스의 생성자가 먼저 호출된다. 이는 자식 클래스 생성자에서 super() 키워드를 사용하여 부모 생성자를 호출하기 때문이다. super()는 명시적으로 작성되지 않더라도, 부모의 기본 생성자가 자동으로 호출된다.
public class Dog extends Animal {
Dog(String name) {
// super(); // 부모 클래스의 기본 생성자 호출 (생략 가능)
System.out.println("Dog 객체 생성: " + name);
}
}
public class DogExample {
public static void main(String[] args) {
Dog dog = new Dog("뽀삐"); // 생성자 호출
}
}
Dog 객체 생성: 뽀삐
super()의 역할부모 클래스에 매개변수가 있는 생성자를 만들면, 자식 클래스에서 super(매개값)으로 부모의 해당 생성자를 명시적으로 호출해야 한다.
public class Person {
String name;
String ssn;
Person(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
}
자식 클래스는 부모의 매개변수 생성자를 호출해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
public class Student extends Person {
Student(String name, String ssn) {
super(name, ssn); // 부모 생성자를 호출해야 오류가 발생하지 않음
}
}
super 키워드의 위치super()는 자식 클래스 생성자 내에서 첫 번째 줄에 위치해야 한다. 부모 클래스의 생성자가 먼저 호출된 후에 자식 클래스의 초기화가 이루어져야 하기 때문이다.
public class Student extends Person {
int studentNo;
Student(String name, String ssn, int studentNo) {
super(name, ssn); // 부모 생성자 호출
this.studentNo = studentNo;
}
}
부모 생성자 호출: 자식 클래스의 생성자가 호출될 때, 부모 클래스의 생성자가 먼저 호출된다.
super(): 부모 클래스의 기본 생성자를 호출하며, 매개변수가 있을 경우 명시적으로 호출해야 한다.
super 위치: 자식 클래스 생성자 내에서 반드시 첫 번째 줄에 작성해야 한다.
Employee 클래스 상속 실습

package chap07.employee;
public class Employee {
String name;
double calculateSalary(String name){
return 0;
}
public static void main(String[] args) {
FullTimeEmployee fullTimeEmployee = new FullTimeEmployee();
PartTimeEmployee partTimeEmployee = new PartTimeEmployee();
System.out.println(fullTimeEmployee.calculateSalary("Alice",20));
System.out.println(partTimeEmployee.calculateSalary("Bob",80, 5));
}
}package chap07.employee;
public class FullTimeEmployee extends Employee {
double salary;
double calculateSalary(String name ,double salary){
System.out.println(name + "'s Salary :" + salary);
return salary;
}
}
package chap07.employee;
public class PartTimeEmployee extends Employee {
double hourlyRate;
int hoursWorked;
double calculateSalary(String name, double hourlyRate, int hoursWorked){
System.out.println(name + "'s Salary :" + hourlyRate * hoursWorked);
return hourlyRate * hoursWorked;
}
}package chap07.employee;
public class Employee {
String name;
public Employee(String name) {
this.name = name;
}
// get 메서드 사용 시
public String getName() {
return name;
}
double calculateSalary() {
return 0;
}
}
package chap07.employee;
public class FullTimeEmployee extends Employee {
double salary;
FullTimeEmployee(String name, double salary){
super(name); // 생성자 호출, 부모에서 받는 생성자 호출될 수 있게
this.salary = salary;
}
// 메서드 재정의 Override
double calculateSalary(){
return salary;
}
}
package chap07.employee;
public class PartTimeEmployee extends Employee {
double hourlyRate;
int hoursWorked;
PartTimeEmployee(String name, double hourlyRate, int hoursWorked){
super(name); // -> this.name = name; 처럼 직접 접근해서 할당해줄 수 있다.
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
// 메서드 재정의(Override)
@Override // 컴파일러한테 알려주는 역할
double calculateSalary(){
return hourlyRate * hoursWorked;
}
}
package chap07.employee;
public class EmployeeExample {
public static void main(String[] args) {
// 객체 생성
FullTimeEmployee alice = new FullTimeEmployee("Alice",4000);
PartTimeEmployee bob = new PartTimeEmployee("Bob", 1000, 4);
// 출력(객체 내부 요소들 호출)
System.out.println(alice.name + "'s Salary : " + alice.calculateSalary());
System.out.println(bob.name + "'s Salary : " + bob.calculateSalary());
// get 메서드 사용 시
System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
}
}오버라이딩은 부모 클래스에 있는 메서드를 동일한 시그니처로 재정의하는 것이다. 즉, 메서드의 리턴 타입, 이름, 매개변수가 부모 클래스와 동일해야 한다.
public class Parent {
void method1() {
System.out.println("Parent method 1");
}
void method2() {
System.out.println("Parent method 2");
}
}
public class Child extends Parent {
@Override
void method2() { // 부모 메서드를 재정의
System.out.println("Child method 2");
}
void method3() {
System.out.println("Child method 3");
}
}
public class ChildExample {
public static void main(String[] args) {
Child child = new Child();
child.method1(); // Parent method 호출
child.method2(); // 재정의된 Child method 호출
child.method3(); // Child에서 추가된 method 호출
}
}
Parent method 1
Child method 2
Child method 3
public → private 불가).오버로딩은 같은 이름의 메서드를 다른 매개변수 리스트로 여러 번 정의하는 것이다. 오버로딩은 메서드의 매개변수 타입이나 개수가 달라야 한다.
public class Calculator {
// 매개변수 개수가 다른 오버로딩
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
// 매개변수 타입이 다른 오버로딩
double add(double a, double b) {
return a + b;
}
}
public class OverloadingExample {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3)); // int 매개변수
System.out.println(calc.add(1, 2, 3)); // 3개 int 매개변수
System.out.println(calc.add(1.5, 2.5)); // double 매개변수
}
}
8
6
4.0
오버로딩은 같은 이름의 메서드를 다양한 방식으로 호출할 수 있어 유연성을 제공한다.
추상 클래스는 추상 메서드를 하나 이상 포함하며, 이 자체로는 객체를 생성할 수 없다. 이를 상속받은 구체적인 클래스에서만 객체를 생성할 수 있다.
abstract class Animal {
abstract void sound(); // 추상 메서드, 자식 클래스에서 반드시 재정의
}
public abstract class Animal {
String kind;
public void breathe() {
System.out.println("숨을 쉽니다.");
}
public abstract void sound(); // 추상 메서드
}
public class Dog extends Animal {
public Dog() {
this.kind = "포유류";
}
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Cat extends Animal {
public Cat() {
this.kind = "포유류";
}
@Override
public void sound() {
System.out.println("야옹");
}
}
public class AnimalExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.sound(); // 멍멍
cat.sound(); // 야옹
}
}
멍멍
야옹
추상 클래스는 상속받는 클래스들에 공통적인 동작을 강제함으로써 설계 규격을 통일하고 유지보수를 쉽게 한다.

package chap07.employee2;
// Employee 추상클래스로 변경
// calculateSalary() 메서드를 추상메서드로 변경
public abstract class Employee {
private String name;
public Employee(String name) {this.name = name;}
public String getName() {
return name;
}
abstract double calculateSalary();
}package chap07.employee2;
public class FullTimeEmployee extends Employee {
double salary;
FullTimeEmployee(String name, double salary){
super(name); // 생성자 호출, 부모에서 받는 생성자 호출될 수 있게
this.salary = salary;
}
@Override
double calculateSalary(){
return salary;
}
}package chap07.employee2;
public class PartTimeEmployee extends Employee {
double hourlyRate;
int hoursWorked;
// 예제 코드
PartTimeEmployee(String name, double hourlyRate, int hoursWorked){
super(name); // -> this.name = name; 처럼 직접 접근해서 할당해줄 수 있다.
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
// 메서드 재정의(Override)
@Override // 컴파일러한테 알려주는 역할
double calculateSalary(){
return hourlyRate * hoursWorked;
}
}
package chap07.employee2;
public class EmployeeExample {
public static void main(String[] args) {
// 객체 생성
FullTimeEmployee alice = new FullTimeEmployee("Alice",4000);
PartTimeEmployee bob = new PartTimeEmployee("Bob", 1000, 4);
System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
/* 객체 지향 특징 - 다형성 */
// Employee employee = new Employee(); 추상 클래스로 객체 생성 불가
Employee fullTimeEmployee = new FullTimeEmployee("",45); // 객체 생성 가능, FullTimeEmployee 의 객체이기 때문에
Employee partTimeEmployee = new PartTimeEmployee("Bob",1000,4); // partTimeEmployee의 객체. employee로 감쌀 수 있는 객체
}
// 추상 클래스의 타입으로 감싸줄 수 있다. 추상화된 클래스나 메서드가 파라미터로 받아지게 된다면 유연해지는 특징이 있다.
void test(Employee employee){
employee.calculateSalary();
}
void test(FullTimeEmployee emp){
}
void test(PartTimeEmployee emp){
}
}