[JAVA의 정석] 객체지향프로그래밍
객체지향 프로그래밍
객체지향 언어의 특징
- 코드의 재사용성이 높다 : 새로운 코드를 작성할 때 기존의 코드를 이용하여 쉽게 작성할 수 있다.
- 코드의 관리가 용이하다 : 코드간의 관계를 이용해서 적은 노력으로 쉽게 코드를 변경할 수 있다.
- 신뢰성이 높은 프로그래밍을 가능하게 한다: 제어자와 메서드를 이용해서 데이터를 보호하고 올바른 값을 유지하도록 하며, 코드의 중복을 제거하여 코드의 불일치로 인한 오동작을 방지할 수 있다.
클래스와 객체의 정의와 용도
- 클래스의 정의 : 클래스란 객체를 정의해 놓은 것이다.
- 클래스의 용도 : 클래스는 객체를 생성하는데 사용된다.
인스턴스
클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화라고 하며, 어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스(instance)라고 한다.
객체의 구성요소
속성(property) : 멤버변수, 특성, 필드, 상태
기능(function) : 메서드, 함수, 행위
xClass Tv {
String color; //색깔
boolean power; // 전원상태
int channel; // 채널
void power() { power = !power;}
void channelUp() { channel ++;}
void channelDown() { channel --;}
}
속성 ( 크기, 길이, 높이, 색상, 볼륨, 채널 등 ) , 기능 ( 켜기, 끄기, 볼륨 높이기, 볼륨 낮추기 )
인스턴스의 생성과 사용
클래스명 변수명; // 클래스의 객체를 참조하기 위한 참조변수 선언
변수명 = new 클래스명 (); // 클래스의 객체를 생성 후, 객체의 주소를 참조변수에 저장
인스턴스는 참조변수를 통해서만 다룰 수 있으며, 참조변수의 타입은 인스턴스의 타입과 일치해야한다.
- 변수 : 하나의 데이터를 저장할 수 있는 공간
- 배열 : 같은 종류의 여러 데이터를 하나의 집합으로 저장할 수 있는 공간
- 구조체: 서로 관련된 여러 데이터를 종류에 관계없이 하나의 집합으로 저장할 수 있는 공간
- 클래스: 데이터와 함수의 결합 ( 구조체 + 함수)
변수와 메서드
변수
xxxxxxxxxx
public class Chapter6 {
/* 클래스 영역 */
int iv; // 인스턴스 변수
static int cv; // 클래스 변수 ( static 변수, 공유 변수)
/* 메서드 영역 */
void method() {
int lv = 0; // 지역 변수
}
}
변수의 종류 | 선언위치 | 생성시기 |
---|---|---|
클래스 변수 | 클래스 영역 | 클래스가 메모리에 올라갈 때 |
인스턴스 변수 | 클래스 영역 | 인스턴스가 생성되었을 때 |
지역변수 | 클래스 영역 이외의 영역 ( 메서드 , 생성자, 초기화 블럭 내부) | 변수 선언문이 수행되었을 때 |
인스턴스변수는 인스턴스가 생성될 때 마다 생성되므로 인스턴스마다 각기 다른 값을 유지할 수 있지만,
클래스 변수는 모든 인스턴스가 하나의 저장공간을 공유하므로, 항상 공통된 값을 갖는다.
메서드
메서드(method)는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다.
메서드를 사용하는 이유
- 높은 재사용성 : 한번 만들어 놓으면 몇 번이고 호출이 가능하다.
- 중복된 코드의 제거
- 프로그램의 구조화 : 메서드를 사용하지 않으면 모든 코드를 main 에 넣는 식이었지만, 큰 프로그램에서는 효율이 낮아지므로 메서드를 통해 구조화 시켜 효율성을 높인다.
xxxxxxxxxx
int add( int a, int b) { // 선언부
int result = a + b; /*
return result; // 호출한 메서드로 결과를 반환 구현부
*/
}
지역 변수( local variable)
메서드 내에 선언된 변수들은 그 메서드 내에서만 사용할 수 있으므로 서로 다른 메서드라면 같은 이름의 변수를 선언해도 된다.
이처럼 메서드 내에 선언된 변수를 '지역변수' 라고 한다.
return 문
메소드의 반환타입이 void 일 경우 컴파일러가 자동적으로 return 을 해주기 때문에 생략이 가능하다.
JVM메모리 구조
xxxxxxxxxx
class CallStackTest {
public static void main(String[] args) {
firstMethod();
}
static void firstMethod() {
secondMethod();
}
static void secondMethod() {
System.out.println("secondMethod()");
}
}
- main 문이 시작된다.
- main 문에있는 firstMethod() 가 실행된다.
- firstMethod() 로 넘어가서 secondMethod()를 호출한다.
- secondMethod()에서 "secondMethod()" 를 출력한다.
- firstMethod()로 돌아간다.
- main 문으로 돌아간다.
기본형 매개변수: 변수의 값을 읽기만 할 수 있다.
참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다.
참조형 반환타입
xxxxxxxxxx
class Data { int x; }
class ReferenceReturnEx {
public static void main(String[] args)
{
Data d = new Data();
d.x = 10;
Data d2 = copy(d);
System.out.println("d.x ="+d.x);
System.out.println("d2.x="+d2.x);
}
static Data copy(Data d) {
Data tmp = new Data();
tmp.x = d.x;
return tmp;
}
}
- copy 메서드를 호출하면서 참조변수 d의 값이 매개변수 d에 복사된다.
- 새로운 객체를 생성한 다음, d,x에 저장된 값을 tmp.x에 복사한다.
- copy메서드가 종료되면서 반환한 tmp의 값은 참조변수 d2에 저장된다.
- copy메서드가 종료되어 tmp가 사라졌지만, d2로 새로운 객체를 다룰 수 있다.
재귀 호출
xxxxxxxxxx
void method() {
method(); // 재귀호출. 메서드 자신을 호출한다.
}
메서드 내부에서 메서드 자신을 다시 호출하는 것을 ' 재귀호출'이라 하고, 재귀호출을 하는 메서드를 '재귀 메서드' 라고 한다.
오버로딩(overloading)
한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 오버로딩 이라고 한다.
오버로딩을 하려면
- 메서드 이름이 같아야 한다.
- 매개변수의 개수 또는 타입이 달라야 한다.
생성자
생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메서드' 이다. 따라서 인스턴스 변수의 초기화 작업에 주로 사용되며, 인스턴스 생성 시에 실행되어야 할 작업을 위해서도 사용된다.
생성자의 조건은
- 생성자의 이름은 클래스의 이름과 같아야 한다.
- 생성자는 리턴 값이 없다.
xxxxxxxxxx
Card c = new Card();
- 연산자 new에 의해서 메모리(heap)에 Card클래스의 인스턴스가 생성된다.
- 생성자 Card()가 호출되어 수행된다.
- 연산자 new 의 결과로, 생성된 Card 인스턴스의 주소과 반환되어 참조변후 c 에 저장된다.
기본 생성자(default constructor)
클래스에 생성자를 정의하지 않고도 인스턴스를 생성할 수 있었던 이유는 컴파일러가 제공하는 '기본 생성자' 덕분이다.
xxxxxxxxxx
class Data1 {
int value;
}
class Data2 {
int value;
Data2(int x) { // 매개변수가 있는 생성자.
value = x;
}
}
class ConstructorTest {
public static void main(String[] args) {
Data1 d1 = new Data1();
Data2 d2 = new Data2(); // compile error발생
}
}
xxxxxxxxxx
Data1 d1 = new Data1();
Data2 d2 = new Data2(); // 에러
위의 코드에서Data2 d2 = new Data2(); 부분이 에러가 난다. 왜냐하면 기본생성자가 아닌 Data2(int x)가 정의되어 있기 때문이다.
this(), this
같은 클래스의 멤버들 간에 서로 호출할 수 있는 것처럼 생성자 간에도 서로 호출이 가능하다.
다음의 조건을 따른다면,
- 생성자의 이름으로 클래스이름 대신 this를 사용한다.
- 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.
this
- 인스턴스 자신을 가리키는 참조변수, 인스턴스의 주소가 저장되어 있다.
- 모든 인스턴스메서드에 지역변수로 숨겨진 채로 존재한다.
this(), this(매개변수)
- 생성자, 같은 클래스의 다른 생성자를 호출할 때 사용한다.
상속(inheritance)
상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
xxxxxxxxxx
class Child extends Parent {
.......
}
조상 클래스 : 부모(Parent)클래스, 상위(Super)클래스, 기반(Base)클래스
자손 클래스: 자식(Child)클래스, 하위(Sub)클래스, 파생된(Derived)클래스
xxxxxxxxxx
class Tv {
boolean power; // 전원상태(on/off)
int channel; // 채널
void power() { power = !power; }
void channelUp() { ++channel; }
void channelDown() { --channel; }
}
class CaptionTv extends Tv {
boolean caption; // 캡션상태(on/off)
void displayCaption(String text) {
if (caption) { // 캡션 상태가 on(true)일 때만 text를 보여 준다.
System.out.println(text);
}
}
}
class CaptionTvTest {
public static void main(String args[]) {
CaptionTv ctv = new CaptionTv();
ctv.channel = 10; // 조상 클래스로부터 상속받은 멤버
ctv.channelUp(); // 조상 클래스로부터 상속받은 멤버
System.out.println(ctv.channel);
ctv.displayCaption("Hello, World");
ctv.caption = true; // 캡션기능을 켠다.
ctv.displayCaption("Hello, World"); // 캡션을 화면에 보여 준다.
}
}
실행결과로
11
Hello, World
가 되며
CaptionTv에는 Tv의 ( power() , power ,channelUp(), channel, channelDown()) 가 있으며, caption과 displayCaption()의 메소드도 있는 상태이다,
즉, 자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
is, has
xxxxxxxxxx
class Circle {
Point c = new Point();
int r;
}
위의 경우에는 Circle이 Point() 를 생성시켜 가지고 있는 'has' 이며 , Circle은 Point를 가지고 있다.
xxxxxxxxxx
class Circle extedns Point {
int r;
}
위의 경우는 Circle 이 Point를 상속하고 있으므로 'is' 이다
즉, Circle은 Point이다.
xxxxxxxxxx
class DrawShape {
public static void main(String[] args) {
Point[] p = { new Point(100, 100),
new Point(140, 50),
new Point(200, 100)
};
Triangle t = new Triangle(p);
Circle c = new Circle(new Point(150, 150), 50);
t.draw(); // 삼각형을 그린다.
c.draw(); // 원을 그린다.
}
}
class Shape {
String color = "black";
void draw() {
System.out.printf("[color=%s]%n", color);
}
}
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
this(0,0);
}
String getXY() {
return "("+x+","+y+")"; // x와 y의 값을 문자열로 반환
}
}
class Circle extends Shape {
Point center; // 원의 원점좌표
int r; // 반지름
Circle() {
this(new Point(0, 0), 100); // Circle(Point center, int r)를 호출
}
Circle(Point center, int r) {
this.center = center;
this.r = r;
}
void draw() { // 원을 그리는 대신에 원의 정보를 출력하도록 했다.
System.out.printf("[center=(%d, %d), r=%d, color=%s]%n", center.x, center.y, r, color);
}
}
class Triangle extends Shape {
Point[] p = new Point[3];
Triangle(Point[] p) {
this.p = p;
}
void draw() {
System.out.printf("[p1=%s, p2=%s, p3=%s, color=%s]%n", p[0].getXY(), p[1].getXY(), p[2].getXY(), color);
}
}
위의 예제에서
A Circle is a Shape // 'is'
A Circle has a Point // 'has'
가 되며 상속관계와 포함관계를 이해 할 수 있다.
Object 클래스
Object 클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속 받지 않는 모든 클래스들을 자동적으로 Object 클래스로부터 상속받게 함으로써 이것을 가능하게 한다.
우리가 평소에 클래스를 만들때도 사실은 extends Obejct 가 생략되어 있으므로 모든 클래스들은 Object의 상속을 받고 있는 상태이다.
오버라이딩(overriding)
오버라이딩은 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 칭한다.
오버라이딩 조건
이름이 같아야 한다.
매개변수가 같아야 한다
반환타입이 같아야 한다.
오버라이딩 할 때 주의해야 할 점
- 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
- 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
- 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.
오버로딩 과 오버라이딩 용어 정리!
- 오버로딩 : 기존에 없는 새로운 메서드를 정의하는 것 (new)
- 오버라이딩 : 상속받은 메서드의 내용을 변경하는 것(change, modify)
super
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다.
멤벼변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 클래스에 정의된 멤버의 이름이 같을 때는 super를 붙여서 구별할 수 있다.
xxxxxxxxxx
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);
}
}
실행결과 this.x 에서는 int x = 20; 의 20이 출력되며 ,
super.x 에서는 parent의 int x = 10; 의 10 이 출력된다.
super()
super()는 조상 클래스의 생성자를 호출하는데 사용된다.
Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super(), 를 호출해야 한다.
그렇지 않으면 컴파일러가 자동적으로 'super()';를 생성자의 첫줄에 삽입한다.
인스턴스를 생성할 때는 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요하다
- 클래스 : 어떤 클래스의 인스턴스를 생성할 것인가?
- 생성자 : 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것인가?
xxxxxxxxxx
class PointTest2 {
public static void main(String argsp[]) {
Point3D p3 = new Point3D();
System.out.println("p3.x=" + p3.x);
System.out.println("p3.y=" + p3.y);
System.out.println("p3.z=" + p3.z);
}
}
class Point {
int x=10;
int y=20;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Point3D extends Point {
int z=30;
Point3D() {
this(100, 200, 300); // Point3D(int x, int y, int z)
}
Point3D(int x, int y, int z) {
super(x, y); // Point(int x, int y)
this.z = z;
}
}
만약 여기서 Point3D p3 = new Point3D() ; 를 생성하면
xxxxxxxxxx
Point3D() {
this(100, 200, 300); // Point3D(int x, int y, int z)
}
this(100,200,300) 이 Point3D(int x, int y, int z) 를 가르킨다.
xxxxxxxxxx
Point3D(int x, int y, int z) {
super(x, y); // Point(int x, int y)
this.z = z;
}
super(x , y) 가 Point(int x, int y) 를 가르킨다
xxxxxxxxxx
Point(int x, int y) {
this.x = x;
this.y = y;
}
제어자
static
static 은 '클래스의' 또는 '공통적인' 의미를 가지고 있다. 인스턴스변수는 하나의 클래스로부터 생성되었더라도 각기 다른 값을 유지하지만, 클래스변수(static 멤버변수)는 인스턴스에 관계없이 같은 값을 갖는다.
static이 사용될 수 있는곳 ( 멤버변수, 메서드, 초기화 블럭)
final
final은 '마지막의' 또는 '변경될 수 없는'의 의미를 가지고 있으며 거의 모든 대상에 사용 될 수 있다.
fianl이 사용될 수 있는 곳 (클래스, 메서드, 멤버변수, 지역변수)
abstarct
abstract 는 '미완성'의 의미를 가지고 있다. 메서드의 선언부만 작성하고 실제 수행내용은 구현하지 않은 추상 메서드를 선언하는데 사용된다
abstract가 사용될 수 있는 곳( 클래스, 메서드)
다형성
객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현한다.
추상클래스
추상클래스는 미완성 설계도 같은 개념이다. 추상클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서 중요한 의미를 갖는다.
사용법은 클래스 앞에 abstract 를 붙이면 된다
xxxxxxxxxx
abstract class 클래스이름 {
abstract void play(int pos); // 추상메서드도 가능하다.
}
인터페이스
인터페이스 역시 추상클래스와 비슷한 개념이지만 추상클래스보다 더 추상화 되어있는 것을 뜻한다.
만약 추상클래스가 미완성 설계도라고 하면 인터페이스는 설계도 조차 존재하지 않은 밑그림 만 그린 설계도라고 볼 수 있다 .
사용법은 앞 부분에 interface를 붙이면 된다
interface 인터페이스 이름 {
public static final 타입 상수이름 = 값;
public abstract 메서드이름 ( 매개변수목록);
}
인터페이스의 이해
- 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
- 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다.
xxxxxxxxxx
class A {
void autoPlay(I i) {
i.play();
}
}
interface I {
public abstract void play();
}
class B implements I {
public void play() {
System.out.println("play in B class");
}
}
class C implements I {
public void play() {
System.out.println("play in C class");
}
}
class InterfaceTest2 {
public static void main(String[] args) {
A a = new A();
a.autoPlay(new B()); // void autoPlay(I i)
a.autoPlay(new C()); // void autoPlay(I i)
}
}
InterfaceTest2에서 A의 생성자를 만들고 A의 메서드에서는 밑그림만 그려져있는 interface I 를 호출한다.
I가 호출하므로서 play()메서드를 실행할 수 있게 해준다.
내부 클래스(inner class)
내부 클래스는 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스를 선언하는 이유는 한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있다는 장점과 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점을 가질 수 있다.
class A {
...
}
class B {
...
}
//위의 코드를 아래로 내부클래스 선언이 된다.
class A {
...
class B {
... }
....
}
class InnerEx1 {
class InstanceInner {
int iv = 100;
// static int cv = 100; // 에러! static변수를 선언할 수 없다.
final static int CONST = 100; // final static은 상수이므로 허용한다.
}
static class StaticInner {
int iv = 200;
static int cv = 200; // static클래스만 static멤버를 정의할 수 있다.
}
void myMethod() {
class LocalInner {
int iv = 300;
// static int cv = 300; // 에러! static변수를 선언할 수 없다.
final static int CONST = 300; // final static은 상수이므로 허용
}
}
public static void main(String args[]) {
System.out.println(InstanceInner.CONST);
System.out.println(StaticInner.cv);
}
}
위의 코드처럼 내부 클래스는 접근제어자들을 사용 할 수 있다.
class Outer {
class InstanceInner {
int iv=100;
}
static class StaticInner {
int iv=200;
static int cv=300;
}
void myMethod() {
class LocalInner {
int iv=400;
}
}
}
class InnerEx4 {
public static void main(String[] args) {
// 인스턴스클래스의 인스턴스를 생성하려면
// 외부 클래스의 인스턴스를 먼저 생성해야 한다.
Outer oc = new Outer();
Outer.InstanceInner ii = oc.new InstanceInner();
System.out.println("ii.iv : "+ ii.iv);
System.out.println("Outer.StaticInner.cv : " + Outer.StaticInner.cv);
// 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
Outer.StaticInner si = new Outer.StaticInner();
System.out.println("si.iv : "+ si.iv);
}
}