프로그래밍에서 상속의 개념은 아래와 같다.
기존 클래스의 멤버를 물려 받아 자식 클래스에서 재사용하는 것이다.
코드를 재사용하여 유지보수가 쉽고 중복이 적다는 장점이 있다.
is a 관계로 정의된다.
Java에서 상속을 사용하는 방법은 아래와 같다.
extends 키워드로 상속을 사용한다.
단, 부모의 생성자와 초기화 블록은 상속하지 않는다.
Java에서 상속의 특징은 아래와 같다.
- 모든 클래스는 Object 클래스를 상속받는다.
- 단일 상속만 지원한다.
※ 단일 상속은 다양한 기능을 지원하지 못한다.
따라서 이를 극복하기 위해 Java는 인터페이스와 has a 관계를 지원한다.
has a 관계란 객체가 객체 안에 객체를 생성하여 해당 객체의 멤버를 사용하는 관계이다.
조상 클래스에 정의된 메서드를 자식 클래스에서 적합하게 수정하는 걸 메서드 오버라이딩이라 한다.
메서드 오버라이딩의 조건은 아래와 같다.
- 메서드 이름이 같아야 한다.
- 매개 변수의 개수, 타입, 순서가 같아야 한다.
- 리턴 타입이 같아야 한다.
- 접근 제한자는 부모보다 범위가 넓거나 같아야 한다.
- 조상보다 더 큰 예외를 던질 수 없다.
컴파일러에게 알려주는 주석을 Annotation이라 한다.
Annotation 예시는 아래와 같다.
- @Deprecated : 컴파일러에게 해당 메서드가 오래되서 향후 없어질 기능임을 걸 알려준다.
- @Override : 컴파일러에게 해당 메서드가 오버라이드한 메서드임을 알려준다.
이때 해당 메서드가 조상 클래스에 없을 시 오류가 발생한다.- @SuppressWarnings : 컴파일러에게 Warning은 신경 쓰지 말라고 알려준다.
따라서 Object 클래스에는 꼭 필요한 메서드들이 정의되어 있다.
- toString() : 객체의 정보를 출력하는 메서드
- equals() : 두 객체의 내용을 비교하는 메서드
위 함수들은 자식 객체에서 적절하게 오버라이딩해서 사용한다.
toString()의 예시는 아래와 같다.
class Sam {
int num;
String name;
@Override
public String toString() {
return "Sam [num=" + num + ", name=" + name + ", toString()=" + super.toString() + "]";
}
}
public class RTest {
public RTest() {
Sam a = new Sam();
a.num = 1;
a.name = "dddd";
System.out.println(a.toString());
}
public static void main(String[] args) {
new RTest();
}
}
toString()을 재정의하여 a 객체의 정보를 출력할 수 있다.
equals()의 예시는 아래와 같다.
class PData {
int a;
PData(int a) {
this.a = a;
}
@Override
public boolean equals(Object obj) {
return a == ((PData)obj).a;
}
}
public class QTest {
public QTest() {
String s1 = new String("samsung");
String s2 = new String("samsung");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
String s3 = "samsung";
String s4 = "samsung";
System.out.println(s3 == s4);
System.out.println(s3.equals(s4));
PData p1 = new PData(1);
PData p2 = new PData(1);
System.out.println(p1 == p2);
System.out.println(p1.equals(p2));
}
public static void main(String[] args) {
new QTest();
}
}
출력 결과는 아래와 같다.
false
true
true
true
false
true
equals()를 재정의하여 p1과 p2 객체의 내용을 비교할 수 있다.
※ == 은 두 객체의 주소를 비교한다.
super 키워드는 자식 객체에서 조상 객체의 멤버를 접근하거나 적절한 조상 생성자를 호출하는 데 사용한다.
첫번째로 자식 객체에서 조상 객체의 멤버를 접근하는 경우이다.
public class Person
...
void jump() {
System.out.println("폴짝");
}
...
public class SpiderMan extends Person
...
@Override
void jump() {
if (isSpider) {
spider.jump();
} else {
super.jump();
}
}
...
else 문이 수행되면 super 키워드를 통해 Person의 jump() 를 수행할 수 있다.
두번째로 자식 객체에서 조상 객체의 생성자를 호출하는 경우이다.
public class Person
...
public Person() {
}
public Person(String name) {
this.name = name;
}
...
public class SpiderMan extends Person
...
public SpiderMan(String name, boolean isSpider) {
super(name);
this.isSpider = isSpider;
spider = new Spider();
}
...
SpiderMan(String name, boolean isSpider) 생성자가 실행되면 super 키워드를 통해
Person(String name) 생성자가 실행된다.
여기서 생각할 점이 몇 가지 있다.
첫번째로 변수의 범위이다. 아래의 코드를 살펴보자.
class Parent {
String x = "parent";
}
class Child extends Parent {
String x = "child";
void method() {
String x = "method";
System.out.print("x : " + x);
System.out.print(" this.x : " + this.x);
System.out.print(" super.x : " + super.x);
}
}
출력 결과는 x : method this.x : child super.x : parent 이다.
...
void method() {
// String x = "method";
...
이 경우 출력 결과는 x : child this.x : child, super.x : parent 이다.
...
class Child extends Parent {
// String x = "child";
...
이 경우 출력 결과는 x : method this.x : parent, super.x : parent 이다.
...
class Child extends Parent {
// String x = "child";
void method() {
// String x = "method";
...
이 경우 출력 결과는 x : parent this.x : parent, super.x : parent 이다.
두번째로 컴파일러의 처리이다.
super로 생성자를 부를 시 주의해야 할 점은 아래와 같다.
- super()는 생성자 코드의 가장 위어야 한다.
따라서 this()와 super()는 같은 생성자 내부에서 동시에 사용할 수 없다.- 만약 생성자 안에 this(), super() 둘 다 없으면 컴파일러가 super()를 자동으로 삽입한다.
※ static 메서드는 오버라이딩을 지원하지 않는다.
Package의 특징은 아래와 같다.
- . 별로 계층적으로 관리된다. ((ex) a.b.c)
- 코드 중 최상위에 있어야 하며 클래스 별로 단 하나만 존재한다.
그래서 클래스의 Full name은 Package name + Class name 으로 말한다.- Package가 없으면 Default Package로 들어가는데, 권장하지 않는다.
Package의 작명 규칙은 아래와 같다.
- for, if 같은 예약 키워드 & java, vs 같은 몇몇 확장자는 이름으로 사용하지 못한다.
- com.회사명.계열명.부서명 ... 같은 형식을 권장한다.
import의 선언 방법은 아래와 같다.
- import 패키지명.클래스명;
- import 패키지명.*; (하위 패키지는 import 하지 않는다.)
Package 사용 시 알아야 할 점이 있다. 아래 예시를 살펴보자.
package gumi.util;
public class Sam {
int num;
void pr() {
}
}
package gumi.util.class4;
import java.awt.*; // java.awt.Button 권장
import java.util.*; // java.util.List 권장
import gumi.util.Sam;
public class ImportTest {
public ImportTest() {
Sam s = new Sam();
// List list; 오류 발생
Button btn;
java.awt.List btn2 = new java.awt.List();
System.out.println(Math.random());
System.out.println(Math.PI);
}
}
- 왠만하면 *을 권장하지 않는다. List 처럼 충돌이 일어날 수 있다.
- import를 하지 않고도 전체 경로를 넣어 객체를 생성할 수 있다.
단, new 뒤에도 전체 경로를 넣어 객체를 생성해야 한다.- import java.lang.* 은 컴파일러가 자동으로 입력한다.
import static 사용시 static 클래스의 멤버에 접근할 수 있다.
final은 제한자 중 하나이다. 제한자의 예는 아래와 같다.
- public, private 같은 접근 제한자
- static 같은 클래스 멤버를 만들 수 있는 제한자
- abstract 같은 추상 멤버를 만들 수 있는 제한자
- final 같은 요소를 더 이상 수정할 수 없게 하는 제한자
Java에서 바뀔 수 없는 값인 상수를 만들 때 final을 사용한다. 사용 예시는 아래와 같다.
- final class : 더 이상 확장할 수 없음 / 상속 금지
- final method : 더 이상 재정의할 수 없음 / 오버라이딩 금지
- final variable : 더 이상 값을 바꿀 수 없음 / 전체 대문자, 두 단어 사이엔 _ 작명 권장
/ 선언할 때 값을 바로 할당 / 멤버 변수- blank final : 선언 후 단 한번만 값을 할당할 수 있음
/ 매개 변수가 변하지 않도록 보장하기 위해 자주 사용 / 지역 변수- static final : debug 시 사용
final variable과 blank final의 차이는 아래와 같다.
public class FinalTest {
final int a = 99; // final variable
public FinalTest() {
final int b; // blank final
b = 99;
}
public static void main(String[] args) {
new FinalTest();
}
}
접근 제한자의 종류는 아래와 같다.
- public : 같은 클래스, 같은 패키지, 다른 클래스의 자손 클래스, 전체 접근 가능
- protected : 같은 클래스, 같은 패키지, 다른 클래스의 자손 클래스 접근 가능
- default (기본) : 같은 클래스, 같은 패키지 접근 가능
- private : 같은 클래스 접근 가능
메서드 오버라이딩 시 부모 클래스와 접근 제한자 범위가 같거나 넓어야만 가능하다.
아래 예시를 살펴보자.
class Person {
private int age;
private String name;
public int getAge () {
return age;
}
public void setAge (int num) {
// 멤버 변수에 접근할 수 있는 조건
this.age = num;
}
public String getName () {
return name;
}
public void setName (String input) {
// 멤버 변수에 접근할 수 있는 조건
this.name = input;
}
}
public class EncTest {
public EncTest() {
Person p = new Person();
int age;
p.setAge(12);
age = p.getAge();
System.out.println(age);
}
public static void main(String[] args) {
new EncTest();
}
}
멤버 변수는 private, 멤버 메소드는 public 으로 설정함으로써
멤버 변수에 접근할 수 있는 조건을 설정할 수 있다.
여러 개의 객체가 필요 없는 객체를 stateless한 객체라고 한다.
객체를 하나 만들어놓고, 이를 사용만 할 수 있도록 할 때 Singleton 디자인 패턴을 활용한다.
Singleton 디자인 패턴의 조건은 아래와 같다.
- 생성자의 접근 제한자가 private : 외부에서 생성자에 접근할 수 없다.
- 객체를 저장한 멤버 변수의 접근 제한자가 private : 외부에서 멤버 변수를 바꿀 수 없다.
- 객체를 저장한 멤버 변수를 외부에 줄 getter 메서드가 public :
외부에서 객체를 저장한 멤버 변수를 가져올 수 있다.- 객체를 저장한 멤버 변수와 getter 메서드가 static :
외부에서 객체를 생성하지 않고 getter 메서드로 객체를 저장한 멤버 변수를 가져올 수 있다.
Singleton 디자인 패턴 구현은 아래와 같다.
package gumi.util;
public class Manager {
private static Manager manager;
private Manager() {
}
public static Manager getInstance() {
// private에 바로 선언할 수도 있지만
// static 메서드가 불렸을 때 불필요하게 manager에 instance를 생성하는 과정을 막자
if (manager == null) {
manager = new Manager();
}
return manager;
}
public static void pr() {
}
public void add() {
}
public void select() {
}
}
package gumi.util;
public class SingleTest {
public static void main(String[] args) {
Manager.pr();
Manager manager1 = Manager.getInstance();
manager1.add();
manager1.select();
Manager manager2 = Manager.getInstance();
Manager manager3 = Manager.getInstance();
System.out.println(manager2 == manager3);
}
}
출력 결과는 true 이다.
※ import 할 수 있는 java 내부 class 중 메서드가 getInstance() 라고
무조건 Singleton 패턴을 보장하진 않는다.