✅ 1. 상속 (inheritance)
상속은 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것.
코드의 재사용성 높히고, 코드의 중복을 제거함.
새로 작성하고자하는 클래스의 이름 뒤에 상속받고자하는 클래스의 이름을 'extends'와 함께 써주면 됨.
💡생성자와 초기화 블럭은 상속되지 않음. 멤버만 상속됨.
상속 관계 : '~은 ~이다. (is-a)'
포함 관계 : '~은 ~을 가지고 있다. (has-a)'
class Circle extends Shape{ // Circle과 Shape는 상속관계
Point center; // Circle과 Point는 포함관계
int r;
...
}
오버라이딩 : 조상 클래스에 정의된 메서드와 같은 메서드를 자손 클래스에 정의하는 것.
✔️ 단일 상속 (single inheritance)
C++과 다르게 java는 단일 상속만을 허용함. 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 해줌.
class Tv{
boolean power;
int channel;
void power() {power =! power;}
void channelUp() {++channel;}
void channelDown() {--channel;}
}
class VCR{
boolean power;
int counter = 0;
void power() {power =! power;}
void play(){}
void stop(){}
void rew(){}
void ff(){}
}
class TVCR extends Tv{
VCR vcr = new VCR(); //VCR클래스를 포함시켜서 사용함.
void play(){
vcr.play();
}
void stop(){
vcr.stop();
}
void rew(){
vcr.rew();
}
void ff(){
vcr.ff();
}
}
VCR 클래스의 메서드 내용이 변경되더라도 TVCR 클래스의 메서드들이 변경된 내용이 적용되는 결과를 얻을 수 있음.
✔️ Object 클래스 - 모든 클래스의 조상
모든 상속계층도의 최상위에는 Object 클래스가 위치함. toString()이나 equals()과 같은 메서드가 정의되어있음.
✅ 2. 오버라이딩 (overriding)
조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것.
💡자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
- 이름이 같아야함
- 매개변수가 같아야함
- 반환타입이 같아야함
= 선언부가 일치해야함
다만, 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경 가능함.
1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경 불가함.
2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없음
3. 인스턴스메서드를 static 메서드로 또는 그 반대로 변경 불가함.
✔️ 오버로딩 vs 오버라이딩
오버로딩 : 기존에 없는 새로운 메서드를 정의하는 것
오버라이딩 : 상속받은 메서드의 내용을 변경하는 것
✔️ super
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수.
멤버변수와 지역변수의 이름이 같을 때 : this
상속받은 멤버와 자신의 멤버와 이름이 같을 때 : super
class SuperTest2{
public static void main(String args[]){
Child c = new Child();
c.method();
}
}
class Parent{
int x = 10;
}
class Child extends Parent{
int x = 20;
void method(){
System.out.println("x=" + x);
System.out.println("this.x=" + this.x);
System.out.println("super.x=" + super.x);
}
}
// 실행 결과
// x = 20
// this.x = 20
// super.x = 10
✔️ super() - 조상 클래스의 생성자
super()는 조상 클래스의 생성자를 호출하는데 사용됨.
Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super()를 호출해야함.
인스턴스를 생성할 때는 클래스를 선택하는 것 만큼 생성자 선택도 중요함.
✅ 3. package와 import
패키지 : 클래스의 묶음. 물리적으로 하나의 디렉토리
import : 소스코드를 작성할 때, 다른 패키지의 클래스를 사용하기 위해.
✅ 4. 제어자 (modifier)
접근 제어자 : public, protected, default, private
그 외: static, final, abstract, native, transient, synchronized, volatile, strictfp
제어자는 클래스나 멤버변수와 메서드에 주로 사용됨.
✔️ static - 클래스의, 공통적인
static이 붙은 멤버변수와 메서드, 그리고 초기화 블럭은 모두 인스턴스를 생성하지 않고도 사용 가능.
인스턴스 멤버를 사용하지 않는다면, static메서드로 선언하는 것을 고려하기. (더 효율적, 속도 빠름)
✔️ final - 마지막의, 변경될 수 없는
final + 클래스 : 변경될 수 없는 클래스. 다른 클래스의 조상이 될 수 없음.
final + 메서드 : 변경될 수 없는 메서드. 오버라이딩을 통해 재정의 될 수 없음.
final + 변수 : 값을 변경할 수 없는 상수.
✔️ abstract - 추상의, 미완성의
메서드의 선언부만 작성하고 실제 수행내용은 구현하지않은 추상 메서드를 선언할 때 사용함.
abstract 클래스 자체로는 쓸모가 없을지라도, 다른 클래스가 이 클래스를 상속받아서 일부의 원하는 메서드만 오버라이딩해도 됨.
abstract class AbstractTest{ //추상 클래스 (추상 메서드를 포함한 클래스)
abstract void move(); //추상 메서드 (구현부가 없는 메서드)
}
✔️ 접근 제어자 (access modifier)
private : 같은 클래스 내에서만 접근이 가능.
default : 같은 패키지 내에서만 접근이 가능.
protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능.
public : 접근 제한이 없음.
접근 제어자는 클래스의 내부에 선언된 데이터를 보호하기 위해 사용됨. (객체 지향의 캡슐화)
메서드를 변경할 때에도, 오류를 테스트해야 할 범위가 줄어듦.
상속을 통해 확장이 될 것으로 예상되는 클래스라면 멤버에 접근 제한을 두되 private 대신 protected를 사용함.
✅ 5. 다형성 (polymorphism)
여러 가지 형태를 가질 수 있는 능력.
조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있음.

클래스 Tv와 CaptionTv는 서로 상속관계에 있음.
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();
이 경우 Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv 클래스의 멤버들만 사용할 수 있음. 따라서, displayCation()과 caption은 참조변수 t로 사용이 불가능함.
✔️ 참조변수의 형변환
형변환은 상속관계에 있어야지만 가능함.
자손타입 → 조상타입 (Up-casting) : 형변환 생략가능 (= 자손타입의 참조변수를 조상타입의 참조변수에 할당)
조상타입 → 자손타입 (Down-casting) : 형변환 생략불가
형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기에 인스턴스에 아무런 영향 미치지 않음.
단지, 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위를 조절하는 것임.
class CastingTest1{
public static void main(String args[]){
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
fe.water();
car = fe;
// car.water(); // 컴파일 에러! Car타입의 참조변수로는 water()를 호출할 수 없음
fe2 = (FireEngine)car;
fe2.water();
}
}
class Car{
String color;
int door;
void drive(){
System.out.println("drive, Brrrr~");
}
void stop(){
System.out.println("Stop!!!");
}
}
class FireEngine extends Car{
void water() {
System.out.println("water!!!");
}
}
1. Car car = null;
Car타입의 참조변수 car을 선언하고 null로 초기화함.
2. FireEngine fe = new FireEngine();
FireEngine인스턴스를 생성하고 FireEngine 타입의 참조변수(fe)가 참조하도록 함.
3. car = fe; // 조상 타입 ← 자손 타입
참조변수 fe가 참조하고 있는 인스턴스를 참조변수 car가 참조하도록 함. fe의 값이 car에 저장됨. 이 때 두 참조변수의 타입이 다르지만, 업캐스팅이므로 생략가능함. 이제는 참조변수 car를 통해서도 FireEngine인스턴스를 사용할 수 있지만, Car타입이므로 Car클래스의 멤버가 아닌 water은 사용이 불가능함.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유로움. 하지만 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용 안됨.
Fruit fruit = new Apple(); // 업캐스팅
Apple apple = (Apple) fruit; // 다운캐스팅 - OK, 안전!
여기서 fruit이 실제로 Apple 이기 때문에 다운캐스팅이 성공함.
Fruit fruit = new Fruit(); // 조상 타입 인스턴스
Apple apple = (Apple) fruit; // 🚫 런타임 에러 (ClassCastException)!
여기서는 fruit가 실제로 Fruit이기 때문에 실패함. 조상 Fruit 클래스는 Apple 클래스의 color() 메서드가 없기 때문에 참조하려 하면 ClassCastException이 발생함.
(너무 헷갈림,,,,ㅠ)
✔️ instanceof 연산자
위의 상황을 안전하게 처리하기 위해, 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용함.
주로 조건문에 사용되면 boolean 값을 가짐. (true = 검사한 타입으로 형변환이 가능함)
Fruit fruit = new Fruit(); // 조상 타입 인스턴스
if (fruit instanceof Apple) {
Apple apple = (Apple) fruit; // 안전한 다운캐스팅
apple.color();
} else {
System.out.println("fruit는 Apple 타입이 아닙니다!");
}
✔️ 참조변수와 인스턴스의 연결
멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우 (= 오버라이딩), 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용됨.
class BindingTest{
public static void main(String args[]){
Parent p = new Child(); // Parent타입 참조변수 p가 Child 인스턴스 참조.
Child c = new Child();
System.out.println("p.x= " + p.x);
p.method();
System.out.println("c.x= " + c.x);
c.method();
}
}
class Parent{
int x = 100;
void method(){
System.out.println("Parent Method");
}
}
class Child extends Parent{
int x = 200;
void method(){
System.out.println("Child Method");
}
}
// p.x= 100
// Child Method
// c.x= 200
// Child Method
인스턴스 변수에 직접 접근하게 되면, 참조변수의 타입에 따라 사용되는 인스턴스 변수가 달라질 수 있으므로 주의해야함.
따라서, 멤버변수들은 private로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록함.
✔️ 여러 종류의 객체를 배열로 다루기
조상타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로, 매개변수가 조상타입이면 자손타입의 참조변수 어느 것이나 매개변수로 받아들일 수 있음. ( = 공통의 조상을 가진 서로 다른 종류의 객체를 묶을 수 있음)
class Product{
int price;
int bonusPoint;
Product(int price){
this.price = price;
bonusPoint = (int)(price/10.0);
}
Product(){} // 기본 생성자
}
class Tv extends Product{
Tv(){super(100);} //조상클래스의 생성자 Product(int price)를 호출
public String toString(){ return "Tv";}
}
class Computer extends Product{
Computer(){super(200);}
public String toString(){ return "Computer";}
}
class Audio extends Product{
Audio(){super(50);}
public String toString(){ return "Audio";}
}
class Buyer{
int money = 1000;
int bonusPoint = 0;
Product[] item = new Product[10];
int i = 0;
void buy(Product p) {
if(money < p.price){
System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
item[i++] = p;
System.out.println(p + "을/를 구입하셨습니다.");
}
void summary(){
int sum = 0;
String itemList="";
for(int i=0; i<item.length; i++){
if(item[i]==null) break;
sum += item[i].price;
itemList += item[i] + ",";
}
System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다.");
System.out.println("구입하신 제품은 " + itemList + "입니다.");
}
}
class PolyArgumentTest2{
public static void main(String args[]){
Buyer b = new Buyer();
b.buy(new Tv());
b.buy(new Computer());
b.buy(new Audio());
b.summary();
}
}
//Tv을/를 구입하셨습니다.
//Computer을/를 구입하셨습니다.
//Audio을/를 구입하셨습니다.
//구입하신 물품의 총금액은 350만원입니다.
//구입하신 제품은 Tv,Computer,Audio, 입니다.
배열의 크기를 10으로 지정했기 때문에 11개 이상의 제품을 구입할 수 없는 문제점 발생.
Vector 클래스를 사용하면 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경쓰지 않아도 됨.
| 메서드 / 생성자 | 설명 |
| Vector() | 10개의 객체를 저장할 수 있는 Vector 인스턴스를 생성함. 10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가됨. |
| boolean add(Object o) | Vector에 객체를 추가함. 추가에 성공하면 결과값으로 true, 실패하면 false가 반환됨. |
| boolean remove(Object o) | Vector에 저장되어 있는 객체를 제거함. 제거에 성공하면 결과값으로 true, 실패하면 false가 반환됨. |
| boolean isEmpty() | Vector가 비어있는지 검사함. 비어있으면 true, 비어있지 않으면 false를 반환함. |
| Object get(int index) | 지정된 위치(index)의 객체를 반환함. 반환타입이 Object타입이므로 적절한 타입으로의 형변환 필요. |
| int size() | Vector에 저장된 객체의 개수를 반환함. |
✅ 추상클래스 (abstract class)
추상 클래스 = 미완성 설계도
추상클래스로는 인스턴스 생성 불가. 상속을 통해서 자손클래스에 의해서만 완성이 가능.
추상메서드를 사용하는 이유는 상속받는 자손 클래스에서는 이 메서드들을 무조건 구현해주어야하기 때문.(빼먹지 않기 위함)
부모 클래스를 추상 클래스로 선언한 뒤, 자손 클래스에서 추상메서드를 구현하는 형식으로 코드를 작성함.
✅ 인터페이스 (interface)
인터페이스는 일종의 추상 클래스. 다만 추상클래스보다 추상화 정도가 높음.
몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없음.
인터페이스 = 기본 설계도 (구현된 것은 아무 것도 없음)
class 대신 interface를 작성함.
interface 인터페이스 이름{
public static final 타입 상수이름 = 값;
public abstract 메서드이름 (매개변수 목록);
}
1. 모든 멤버 변수는 public static final이어야 하며, 생략 가능
2. 모든 메서드는 public abstract이어야 하며, 생략 가능
인터페이스는 인터페이스로부터만 상속 가능, 다중 상속 가능.
class FighterTest{
public static void main(String args[]){
Fighter f = new Fighter();
if(f instance of Unit)
System.out.println("f는 Unit클래스의 자손입니다.");
if(f instance of Fightable)
System.out.println("f는 Fightable인터페이스를 구현했습니다.");
if(f instance of Movable)
System.out.println("f는 Movable인터페이스를 구현했습니다.");
if(f instance of Attackable)
System.out.println("f는 Attackble인터페이스를 구현했습니다.");
if(f instance of Object)
System.out.println("f는 Unit클래스의 자손입니다.");
}
}
class Fighter extends Unit implements Fightable{
public void move(int x, int y) {}
public void attack(Unit u){}
}
class Unit{
int currentHP;
int x;
int y;
}
interface Fightable extends Movable,Attackable {}
interface Movable {void move(int x, int y);}
interface Attackable{ void attack(Unit u);}

오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야함.
따라서 Fighter 클래스에서는 void move의 접근 제어자를 반드시 public으로 해야함.

인터페이스는 다음과 같이 다른 조상 클래스를 상속하는 클래스들이 Eatable하는 기능을 구현하도록 할 수 있음.
여기서, Talkable 기능은 같은 조상 클래스를 상속하고 있으므로

Human 추상 클래스에 말하는 기능을 강제함으로써 공통 기능의 구현을 강제할 수 있음.
✔️ 인터페이스의 이해
인터페이스를 이해하기 위해 두 가지 사항을 반드시 염두해두어야함.
💡 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있음.
💡 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 됨.

인터페이스를 사용하기 전, 클래스 A와 B는 직접적인 관계임.
class A{
public void methodA(B b){
b.methodB();
}
}
class B{
public void methodB(){
System.out.println("methodB()");
}
}
class InterfaceTest{
public static void main(String args[]){
A a = new A();
a.methodA(new B());
}
}
클래스 A를 작성하려면 클래스 B가 필요함. 클래스 B의 methodB의 선언부가 변경되면 다른 한쪽도 변경된다는 단점.

인터페이스 사용 후. 'A-I-B'의 간접적인 관계로 바뀜. 클래스 B의 변경에 A가 영향을 받지 않음. 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 됨.
class InterfaceTest3{
public static void main(String args[]{
A a = new A();
a.methodA();
}
}
class A{
void methodA(){
I i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString());
}
}
interface I {
public abstract void methodB();
}
class B implements I{
public void methodB(){
System.out.println("methodB in B class");
}
public String toString(){ return "class B";}
}
class InstanceManager { // 제 3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스
public static I getInstance(){
return new B(); // 다른 인스턴스로 바꾸려면 여기만 변경하면 됨.
}
}
인스턴스를 직접 생성하지 않고, getInstance()라는 메서드를 통해 제공받음. 이렇게 하면 다른 클래스의 인스턴스로 변경되어도 A 클래스를 변경하지 않아도 됨.
'JAVA' 카테고리의 다른 글
| JAVA STUDY - CH6 : 객체지향 프로그래밍 - 1 (1) | 2025.01.10 |
|---|---|
| JAVA STUDY - CH 3 : 연산자 operator (0) | 2024.07.01 |
| JAVA STUDY - CH 5 : 배열 Array (0) | 2024.05.09 |
| JAVA STUDY - CH 4 : 조건문과 반복문 (0) | 2024.05.02 |
| JAVA STUDY - CH 1 : 자바를 시작하기 전에 / CH 2 : 변수 ( Variable ) (0) | 2024.05.01 |