Java는 객체지향 언어로써 코드 간에 관계를 맺어 줌으로써 보다 유기적인 프로그램을 구성할 수 있다.
이번에는 이러한 특징을 가진 Java의 대표적인 요소들의 특징과 관계에 대해서 한번 살펴보자
클래스(Class)란?
클래스는 표현하고자 하는 대상의 공통 속성을 한 군데에 정의해 놓은 것이라고 할 수 있다.
즉, 클래스는 객체의 속성을 정의해 놓은 것.
또한 클래스 내부의 정보를 멤버 변수라고 한다.

여기 붕어빵 틀이 있다. 붕어빵 틀은 붕어빵을 만드는데 이용이 된다. 클래스랑 인스턴스도 이와 마찬가지로 생각하면 된다. 붕어빵을 만드는 틀이 곧 클래스가 되며 붕어빵 틀로부터 만들어진 붕어빵이 곧 인스턴스가 된다
인스턴스(Instance)란?
어떠한 클래스로부터 만들어진 객체를 그 클래스의 인스턴스라고 한다.
class Phone {
String model;
String color;
int price;
}
public class Main {
public static void main(String[] args) {
Phone galaxy = new Phone();
galaxy.model = "Galaxy10";
galaxy.color = "Black";
galaxy.price = 100;
Phone iphone =new Phone();
iphone.model = "iPhoneX";
iphone.color = "Black";
iphone.price = 200;
System.out.println("철수는 이번에 " + galaxy.model + galaxy.color + " + 색상을 " + galaxy.price + "만원에 샀다.");
System.out.println("영희는 이번에 " + iphone.model + iphone.color + " + 색상을 " + iphone.price + "만원에 샀다.");
}
}
Phone라는 클래스에는 핸드폰의 모델, 색깔, 가격에 대한 정보가 담겨 있다. 이를 활용하여 model, color, price라는 같은 속성을 가진 galaxy, iphone으로 각기 다른 인스턴스를 만들었다.
메소드(method)란
메소드는 어떠한 작업을 수행하는 코드를 하나로 묶어 놓은 것
메소드가 필요한 이유
1. 재사용성
-메소드를 만들어 놓으면 이후 반복적으로 재사용이 가능하다. 다른 프로그램에서도 사용 가능하다
2. 중복된 코드 제거
-프로그램을 작성하다 보면 같은 코드가 여러 번 반복되어 작성되곤 한다. 이럴 때, 메소드를 활용하면 중복된 부분을
없애므로 보다 효율적인 코드가 된다.
3. 프로그램 구조화
-구조화에 대해서는 아래 예시를 보면서 이해할 수 있다.
int[] heights = new int[5]; // 키가 들어가 있는 배열
initHeight(heights); // 1. 키에 대한 초기화
sortHeight(heights); // 2. 키를 오름차순으로 정렬
printHeight(heights); // 3. 정렬된 키를 출력
-보시다시피 코드가 어떠한 작업을 하느냐에 따라 구분이 되어 구조화가 된 것을 확인할 수 있다. 엄청나게 긴 코드를 작성할 때 이러한 방식을 통해 보다 쉽게 수정 및 관리를 할 수 있다.
-메소드를 만들 때는 메소드 안에서 동작하는 내용을 잘 표현할 수 있도록 이름을 잘 지어주면, 메소드 안을 들여다 보지 않고도 한 눈에 코드를 읽어내려갈 수 있어 좋다. 이것을 readability가 좋다고 표현하는데 이 readability의 기본 품질을 위해서 Java로 메소드를 만들 때 지켜줘야 하는 기본 약속은 다음 두 가지다.
1. 동사로 시작해야 한다.
2.camelcase로 작성해야 한다(첫 단어는 소문자, 이후 단어의 구분에 따라 첫 글자는 대문자)
이제 메소드의 역할과 필요한 이유를 알게 되었으니 어떻게 구현하는지 알아보자
메소드는 다음 형식으로 정의할 수 있다.
반환타입 메소드이름 (타입 변수명,타입 변수명, ...){
수행되어야 할 코드
}
메소드이름은 이름일 것이며 수행되어야 할 코드는 수행 코드이지만 반환 타입이 무엇이지?라는 생각이 들 수 있다.
메소드는 return문을 통해 수행의 결과를 반환하게 되는데 이때, 결과의 자료형을 결정하는 부분이 반환 타입이다.
int add(int x, int y) {
int result = x + y;
return result;
}
메소드의 반환 타입은 int이며 이는 반환되어지는 result와 일치하여야 한다.
class Calculation {
int add(int x, int y) {
int result = x + y;
return result;
}// 두 값을 더한 결과
int subtract(int x, int y) {
int result = x - y;
return result;
}// 두 값을 뺀 결과
}
public class Main {
public static void main(String[] args) {
Calculation calculation = new Calculation();
int addResult = calculation.add(100, 90);
int subResult = calculation.subtract(90, 70);
System.out.println("두 개를 더한 값은 " + addResult);
System.out.println("두 개를 뺀 값은 " + subResult);
}
}
다음은 메소드 예제 코드이다.
Calculation클래스에서 add와 subtract 메소드를 선언하였다. 이는 result (int타입)으로 반환하여 return 한다.
add메소드와 subtract메소드 모두 x와 y변수가 중복되어 사용되었는데, 메소드내의 변수는 지역변수로써 메소드 내부에서만 사용할 수 있는 변수다. 즉 서로 다른 메소드라면 같은 이름의 지역변수를 선언하여 사용해도 문제가 없다.
생성자(constructor)란
메소드를 배웠으니 생성자에 대해 배워보자. 생성자는 인스턴스가 생성될 때 사용되는 '인스턴스 초기화 메소드' 다. 즉
new와 같은 키워드로 해당 클래스의 인스턴스가 새로 생성될 때, 자동으로 호출되는 메소드다. 이 생성자를 이용해서
인스턴스가 생성될 때 수행할 동작을 코드로 짤 수 있는데, 대표적으로 인스턴스 변수를 초기화하는 용도로 사용한다.
클래스이름 (타입 변수명, 타입 변수명, ...){
인스턴스 생성 될 때에 수행하여할 코드
변수의 초기화 코드
}
생성자에게도 생성자만의 조건이 있기 때문에 아래 부분을 따라줘야 한다.
1. 생성자의 이름은 클래스명과 같아야 한다.
2. 생성자는 리턴 값이 없다.
지금까지 생성자에 대해서 모른 채로 프로그래밍을 해왔다. 하지만 모든 클래스에는 반드시 하나 이상의 생성자가 존재해야 한다. 그렇다면 그동안 생성자를 만들지 않았는데 어떻게 오류가 안 났는지 에 대한 의문이 생긴다
사실 클래스에 생성자가 1개도 작성이 되지 않을 경우, 자바 컴파일러가 기본 생성자를 추가해주기 때문에 우리는
기본 생성자를 작성하지 않고도 편리하게 사용할 수 있다.
class Phone {
String model;
String color;
int price;
Phone(String model, String color, int price) {
this.model = model;
this.color = color;
this.price = price;
}
}
public class Main {
public static void main(String[] args) {
Phone galaxy = new Phone("Galaxy10", "Black", 100);
Phone iphone =new Phone("iPhoneX", "Black", 200);
System.out.println("철수는 이번에 " + galaxy.model + galaxy.color + " + 색상을 " + galaxy.price + "만원에 샀다.");
System.out.println("영희는 이번에 " + iphone.model + iphone.color + " + 색상을 " + iphone.price + "만원에 샀다.");
}
}
생성자에서 사용된 this는 생성된 객체 자신을 가리키며 생성자의 매개변수의 값을 객체의 해당하는 데이터에 넣어주게 된다.
상속(inheritance)란

상속을 보여주는 UML Class Diagram이다. 자동차의 하위 계층으로 SUV와 SEDAN이 존재하는데
우리도 이렇게 계층적인 구조를 만들어 보자.
상속이란 기존의 클래스를 재사용하는 방식 중 하나다. 한 번 작성한 코드가 재사용이 필요하다면,
변경사항만 코드로 작성하므로 상대적으로 적은 양의 코드를 작성할 수 있게 된다. 이렇게 코드를
재사용하면, 코드와 클래스가 많아질수록 관리가 용이하다는 장점이 있다.
상속의 특징
1. 부모 클래스에서 정의된 필드와 메소드를 물려받는다.
2. 새로운 필드와 메소드를 추가할 수 있다.
3. 부모 클래스에서 물려받은 메소드를 수정할 수 있다.
class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}
->상속은 extends를 이용하여 사용할 수 있다.

그림에서 Animal은 부모 클래스, 조상 클래스라고 부른다. Dog, Cat클래스는 자식 클래스, 자손 클래스라고 부른다.
class Animal {
String name;
public void cry() {
System.out.println(name + " is crying.");
}
}
class Dog extends Animal {
Dog(String name) {
this.name = name;
}
public void swim() {
System.out.println(name + " is swimming!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("코코");
dog.cry();
dog.swim();
Animal dog2 = dog;
dog2.cry();
// dog2.swim(); // compile error
}
}
자식(Dog) 객체는 자식(Dog) 타입으로 선언된 변수에도 할당할 수 있고, 부모(Animal) 타입으로 선언된 변수에도 할당할 수 있다. 단 부모(Animal) 타입의 변수로 사용할 때는, 실제 객체를 만들(new) 때 사용한 자식(Dog) 타입에 있는 함수(여기서는 swim())을 호출할 수 없다. 컴파일 에러다
<참고로 상속을 받을 때 여러 클래스를 상속받을 수는 없다. 오직 하나의 클래스만을 상속받을 수 있다.>
오버 로딩(overloading)과 오버 라이딩(overriding)
오버 로딩이란?
한 클래스 내에 동일한 이름의 메소드를 여러개 정의하는 것
<동일한 이름의 메소드를 정의한다고 해서 무조건 오버 로딩인 것은 아님!!>
오버 로딩의 조건
->메소드 이름이 동일해야 한다.
->매개변수의 개수 혹은 타입이 달라야 한다.
int add(int x, int y, int z) {
int result = x + y + z;
return result;
}
long add(int a, int b, int c) {
long result = a + b + c;
return result;
}
// 반환타입은 다르지만 매개변수의 자료형과 개수는 같기에 오버로딩이 아닙니다.
int add(int x, int y, int z) {
int result = x + y + z;
return result;
}
long add(int a, int b, long c) {
long result = a + b + c;
return result;
}
int add(int a, int b) {
int result = a + b;
return result;
}
// 오버로딩의 조건에 부합하는 예제입니다.
오버 라이딩이란?
->부모 클래스로부터 상속받은 메소드의 내용을 변경하는 것
->상속받은 메소드를 그대로 사용하기도 하지만, 필요에 의해 변경해야 할 경우 오버 라이딩을 한다.
오버 라이딩의 조건
->부모 클래스의 메소드와 이름이 같아야 한다.
->부모 클래스의 메소드와 매개변수가 같아야 한다.
->부모 클래스의 메소드와 반환 타입이 같아야 한다.
class Animal {
String name;
String color;
public void cry() {
System.out.println(name + " is crying.");
}
}
class Dog extends Animal {
Dog(String name) {
this.name = name;
}
public void cry() {
System.out.println(name + " is barking!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("코코");
dog.cry();
}
}
이 코드의 결과물로 <코코is crying>이 아닌 <코코 si barking>이 나온다. 그 이유는 부모 클래스(Animal)로 부터 상속받은 메소드 cry()의 내용을 바꿨기 때문이다.
두 가지 내용을 정리하면
오버 로딩 : 기존에 없는 새로운 메소드를 정의하는 것
오버 라이딩 : 상속받은 메소드의 내용을 변경하는 것
접근제어자(access modifier)란
접근제어자는 멤버 변수/함수 혹은 클래스에 사용되며 외부에서의 접근을 제한하는 역할을 합니다.
→ private : 같은 클래스 내에서만 접근이 가능
→ default(nothing) : 같은 패키지 내에서만 접근이 가능
→ protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능
→ public : 접근 제한이 전혀 없다.
위의 설명을 토대로 접근 범위에 대해 정리를 하면 다음과 같다.
(좁음) (넓음)
private → default → protected → public
예제를 위해 java 디렉토리에 오른쪽 마우스를 누르고 new → package 를 클릭하고, pkg 라는 이름의 package를 만든다. pkg 디렉토리 내부에 ModifierTest클래스를 만든다

ModifierTest.java
package pkg;
public class ModifierTest {
private void messageInside() {
System.out.println("This is private modifier");
}
public void messageOutside() {
System.out.println("This is public modifier");
messageInside();
}
protected void messageProtected() {
System.out.println("This is protected modifier");
}
}
기존의 Main 클래스와는 달리, 맨 위에 pkg라는 package 선언부가 추가된 것을 확인할 수 있다.
<Java에서 정확한 클래스의 이름은, package 이름까지 포함한 것이 자바 시스템이 인식하는 클래스의 이름이다.
위의 경우 pkg.ModifierTest 라는 이름이 된다. 한 자바 애플리케이션에서 이렇게 패키지 이름까지 같은 클래스 이름은 하나밖에 존재할 수 없다. 즉 pkg.ModifierTest클래스와 pkg2.ModifierTest는 소스코드에서 보이는 클래스의 이름은 같지만 실제로는 다른 클래스다. 다른 클래스를 import 할 때, package이름까지 정확히 확인해야하는 이유가 여기에 있다.
앞의 예제에서 Scanner를 alt+Enter를 통해서 import할 때 IDE가 추천해주는 선택지가 많았는데, 자세히 확인해보면 모두 패키지 이름은 다른 것을 볼 수 있었다.>
Main.java
import pkg.ModifierTest;
class Child extends ModifierTest {
void callParentProtectedMember() {
System.out.println("Call my parent's protected method");
super.messageProtected();
}
}
public class Main {
public static void main(String[] args) {
ModifierTest modifierTest = new ModifierTest();
modifierTest.messageOutside();
// modifierTest.messageInside(); // compile error
// modifierTest.messageProtected(); // compile error
Child child = new Child();
child.callParentProtectedMember();
}
}
위 예제를 실행해보면 access에 대한 컴파일 오류가 발생할 것이다. 이처럼 접근 제어자를 통하여 접근할 수 있는 범위가 제한되곤 한다.
그렇다면 접근 제어자를 사용하는 이유는 무엇일까?
-객체지향 프로그래밍이란 객체들 간의 상호작용을 코드로 표현하는 것.
-이때 객체들 간의 관계에 따라서 접근할 수 있는 것과 아닌 것, 권한을 구분할 필요가 생긴다.
-클래스 내부에 선언된 데이터의 부적절한 사용으로부터 보호하기 위해 (이런 것을 캡슐화(encapsulation)라고 한다.
-접근 제어자는 캡슐화가 가능할 수 있도록 돕는 도구다.
추상 클래스(abstract class)란
추상 클래스는 추상메소드를 선언할 수 있는 클래스를 의미한다. 또한 추상클래스는 클래스와는 다르게 상속받는 클래스 없이 그 자체로 인스턴스를 생성할 수 없다.
먼저 추상메소드에 알아보자면
-추상메소드는 설계만 되어있으며 수행되는 코드에 대해서는 작성이 안된 메소드다.
-이처럼, 미완성으로 남겨두는 이유는 상속받는 클래스마다 반드시 동작이 달라지는 경우에 상속받는 클래스 작성자가
반드시 작성하도록 하기 위함
추상메소드 형식
abstract 리턴타입 메소드이름();
abstract class Bird {
private int x, y, z;
void fly(int x, int y, int z) {
printLocation();
System.out.println("이동합니다.");
this.x = x;
this.y = y;
if (flyable(z)) {
this.z = z;
} else {
System.out.println("그 높이로는 날 수 없습니다");
}
printLocation();
}
abstract boolean flyable(int z);
public void printLocation() {
System.out.println("현재 위치 (" + x + ", " + y + ", " + z + ")");
}
}
class Pigeon extends Bird {
@Override
boolean flyable(int z) {
return z < 10000;
}
}
class Peacock extends Bird {
@Override
boolean flyable(int z) {
return false;
}
}
public class Main {
public static void main(String[] args) {
Bird pigeon = new Pigeon();
Bird peacock = new Peacock();
System.out.println("-- 비둘기 --");
pigeon.fly(1, 1, 3);
System.out.println("-- 공작새 --");
peacock.fly(1, 1, 3);
System.out.println("-- 비둘기 --");
pigeon.fly(3, 3, 30000);
}
}

fly(x,y,z)함수는 Bird를 상속받는 모든 클래스에서 동일한 동작을 한다. 다만, 그 안에서 호출된 flyable(z)의 동작만 그것을 구현하는 자식 클래스에서 구현한 대로 동작하는 것.
공작새(peacok)는 새이지만 전혀 날 수가 없다. 그래서 공작새의 flyable()은 항상 false를 리턴해 언제나 x, y좌표로만 움직인다. 반면 비둘기(pigeon)는 일정 높이까지는 날아갈 수 있기 때문에 그 기준(여기서는 10000)이 되기 전까지는 z좌표로도 움직일 수 있다. 이것을 새의 종류마다 중복 코드 없이 구현하려면 추상 클래스와 추상메소드를 이용해서 이렇게 구현할 수 있다. 이렇게 코드를 짜면, 중복 코드가 없으면서도 새의 종류마다 주어진 위치까지 날 수 있는지를 판단할 수 있는 유연성을 허용하며 구현할 수 있다.
인터페이스(Interface)란
인터페이스는 객체의 특정 행동의 특징을 정의하는 간단한 문법이다. 인터페이스는 함수의 특징(method signautre)인
접근제어자, 리턴 타입, 메소드 이름만을 정의한다. 함수의 내용은 없다. 인터페이스를 구현하는 클래스는 인터페이스에 존재하는 함수의 내용({} 중괄호 안의 내용)을 반드시 구현해야 한다.
인터페이스 형식
interface 인터페이스명{
public abstract void 추상메서드명();
}
interface Bird {
void fly(int x, int y, int z);
}
class Pigeon implements Bird{
private int x,y,z;
@Override
public void fly(int x, int y, int z) {
printLocation();
System.out.println("날아갑니다.");
this.x = x;
this.y = y;
this.z = z;
printLocation();
}
public void printLocation() {
System.out.println("현재 위치 (" + x + ", " + y + ", " + z + ")");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Pigeon();
bird.fly(1, 2, 3);
// bird.printLocation(); // compile error
}
}

interface인 Bird 타입으로 선언한 bird변수는 실제로 Pigeon 객체이지만, interface인 Bird에 선언되지 않은 printLocation()이라는 함수는 호출할 수 없다. interface type으로 선언되어있는 부분에서는 실제 객체가 무엇이든지, interface에 정의된 행동만 할 수 있다.
인터페이스와 추상 클래스 차이
- 인터페이스
- 구현하려는 객체의 동작의 명세
- 다중 상속 가능
- implements를 이용하여 구현
- 메소드 시그니처(이름, 파라미터, 리턴 타입)에 대한 선언만 가능
- 추상 클래스
- 클래스를 상속받아 이용 및 확장을 위함
- 다중 상속 불가능 , 단일 상속
- extends를 이용하여 구현
- 추상메소드에 대한 구현 가능
'공부기록 > Java' 카테고리의 다른 글
| Java 해쉬함수 (0) | 2022.01.15 |
|---|---|
| Java (네트워킹) (0) | 2022.01.10 |
| Java (제네릭스)(람다)(스트림) (0) | 2022.01.09 |
| Java (예외,에러처리)(날짜와 시간 다루기)(컬렉션) (0) | 2022.01.08 |
| Java문법 (변수와 상수)(자료형)(연산자)(조건문과반복문) (0) | 2022.01.05 |