상속 과 캡슐화

박대운·2022년 11월 7일
0

Java

목록 보기
2/9

후기.
오늘도 새로운 내용을 배운다. 점점 더 어렵고 복잡한 내용들을 배우고 있는데 머리에 다 넣는게 쉽지않다... 계속 반복적인 학습이 필요하다... 그래도 겁내지말고 일단 부딪혀 보자!! 이제 2주밖에 되지 않았다!!

상속 과 캡슐화

상속

자바 언어에서 상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미한다. 두 클래스를 서로 상속 관계를 만들며, 하위 클래스는 상위 클래스가 가진 모든 멤버를 상속받게 된다. 하위 클래스의 멤버 개수는 언제나 상위 클래스의 그것과 비교했을 때 같거나 많다.

왜, 언제 사용할까?

코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있습니다. 더 나아가, 상속은 다형적 표현이 가능하다는 장점이 있습니다. (다형성)

마지막으로 언급하고 싶은 내용은 자바의 객체지향 프로그래밍에서는 단일 상속(single inheritance)만을 허용한다는 것입니다. 다른 말로, 다중 상속은 허용되지 않습니다.

어떻게 사용할까?
만든 class 뒤에 상속받을 class를 extends를 통해 상속받을 수 있다.
public class Son extends Parents {

메서드 오버라이딩

메서드 오버라이딩(Method Overriding)은 상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것을 의미합니다.

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}

public class Bike extends Vehicle { // Vehicle 클래스 상속
    void run() {
        System.out.println("Bike is running"); // 메서드 오버라이딩
    }

    public static void main(String[] args) {
        Bike bike = new Bike();
        bike.run();
    }
}

// 출력값
"Bike is running"

Vehicle is running 을 Bike is running 으로 덮어쓴 모습. run() 메서드를 오버라이딩 한 모습.

메서드 오버라이딩을 사용할 때 다음의 세 가지 조건을 반드시 만족시켜야 한다.

1. 메서드의 선언부(메서드 이름, 매개변수, 반환타입)이 상위클래스의 그것과 완전히 일치해야한다.
2. 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다.
3. 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다.

public class Main {
    public static void main(String[] args) {
        Bike bike = new Bike(); // 각각의 타입으로 선언 + 각각의 타입으로 객체 생성
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
	    bike.run();
        car.run();
        motorBike.run();

	    Vehicle bike2 = new Bike(); // 상위 클래스 타입으로 선언 + 각각 타입으로 객체 생성
        Vehicle car2 = new Car();
        Vehicle motorBike2 = new MotorBike();

        bike2.run();
        car2.run();
        motorBike2.run();
    }
}

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}

class Bike extends Vehicle {
    void run() {
        System.out.println("Bike is running");
    }
}

class Car extends Vehicle {
    void run() {
        System.out.println("Car is running");
    }
}

class MotorBike extends Vehicle {
    void run() {
        System.out.println("MotorBike is running");
    }
}

run() 을 오버라이딩 시키고 Vehicle 을 Bike, Car, MotorBike에 상속 시킨 모습.

배열로 한번에 관리 할수도 있다.

// 배열로 한번에 관리하기

Vehicle[] vehicles = new Vehicle[] { new Bike(), new Car(), new MotorBike()};
for (Vehicle vehicle : vehicles) {
		vehicle.run();
}

// 출력값
Bike is running
Car is running
MotorBike is running

super 키워드와 super()

this, this()와 매우 비슷하다고 할 수 있다. super 키워드는 상위 클래스의 객체, super()는 상위 클래스의 생성자를 호출하는 것을 의미한다.
공통적으로 모두 상위 클래스의 존재를 상정하며 상속 관계를 전제로 합니다.

super

public class Super {
    public static void main(String[] args) {
        Lower l = new Lower();
        l.callNum();
    }
}

class Upper {
    int count = 20; // super.count
}

class Lower extends Upper {
    int count = 15; // this.count

    void callNum() {
        System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count = " + super.count);
    }
}

// 출력값
count = 15
count = 15
count = 20

lower 객체 생성하고 호출 했으니까
일반 count는 가장가까운 15를 출력.
this.count는 lower의 15를 출력.
super.count는 상위클래스를 구분하여 호출하는 것! 20출력.

super()

super()도 this() 메서드처럼 생성자를 호출할 때 사용한다.
this()와의 차이는 this()가 같은 클래스의 다른 생성자를 호출하는데 사용하는 반해, super()는 상위 클래스의 생성자를 호출하는데 사용된다는 것이다.

public class Test {
    public static void main(String[] args) {
        Student s = new Student();
    }
}

class Human {
    Human() {
        System.out.println("휴먼 클래스 생성자");
    }
}

class Student extends Human { // Human 클래스로부터 상속
    Student() {    
        super(); // Human 클래스의 생성자 호출
        System.out.println("학생 클래스 생성자");
    }
}

// 출력값
휴먼 클래스 생성자
학생 클래스 생성자

Human클래스를 인스턴스화 시키지 않고도 Student 클래스로 Human 클래스를 상속 받아, super()로 호출하는 모습.
super() 메서드 또한 this()와 마찬가지로 생성자 안에서만 사용가능하고, 반드시 첫 줄에 와야 합니다.
이때 상위클래스에 기본생성자가 없으면 에러가 발생하게 됩니다.
따라서 클래스를 만들 때는 자동으로 기본 생성자를 생성하는 것을 습관화하는 것이 좋습니다.

클래스의 정점, Object 클래스

Object 클래스는 자바의 클래스 상속계층도에서 최상위에 위치한 상위클래스이다. 따라서 자바의 모든 클래스는 Object 클래스로부터 확장된다는 명제는 항상 참이다.

실제로 자바 컴파일러는 컴파일링의 과정에서 다른 클래스로부터 아무런 상속을 받지 않는 클래스에 자동적으로 extends Object를 추가하여 Object 클래스를 상속받도록 합니다.

class ParentEx {  //  컴파일러가 "extends Object" 자동 추가 

}

class ChildEx extends ParentEx {

}

위의 예시에서, ParentEx 클래스를 상속받아 ChildEx 클래스를 만들었을 때 상위클래스 ParentEx는 아무것도 상속하고 있지 않기에 컴파일러는 extends Object를 삽입하는 것이다.

Object 클래스는 자바 클래스의 상속계층도에 가장 위에 위치하기 때문에 Object 클래스의 멤버들을 자동으로 상속받아 사용할 수 있다.

마지막으로 Object 클래스의 대표적인 몇 가지 메서드를 살펴보도록 하자.

캡슐화

캡슐화란 특정 객체 안에 관련된 속성과 기능을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것을 말한다.

이렇게 캡슐화를 해야하는 이유로 크게 두 가지 목적이 있다. 첫 째는 데이터 보호의 목적이고, 두 번째로 내부적으로만 사용되는 데이터에 대한 불필요한 외부 노출을 방지하기 위함.

캡슐화의 가장 큰 장점은 정보 은닉(data hiding)에 있다고 정리할 수 있다.

패키지

패키지(package)란 특정한 목적을 공유하는 클래스와 인터페이스의 묶음을 의미한다.

클래스를 정의할 때 관련있는 속성과 기능을 묶어 데이터들을 효율적으로 관리할 수 있었듯, 패키지는 클래스들을 그룹 단위로 묶어 효과적으로 관리하기 위한 목적을 가지고 있다.

우리가 컴퓨터를 사용할 때 폴더를 만들어 그 폴더와 관련된 파일들을 관리하는 것과 유사하다고 할 수 있다.

자바에 기본적으로 포함되어있는 대표적인 패키지로 자바의 기본 클래스들을 모아 놓은 java.lang, 확장 클래스를 묶어 놓은 java.util, 자바의 입출력과 관련된 클래스를 묶어놓은 java.io와 java.nio 등이 있다.

Import문

import문은 다른 패키지 내의 클래스를 사용하기 위해 사용하며, 일반적으로 패키지 구문과 클래스문 사이에 작성한다.

import 패키지명.클래스명; 또는 import 패키지명.*;

예제)

package practicepack.test;

public class ExampleImp {
		public int a = 10;
		public void print() {
			System.out.println("Import문 테스트")
}
package practicepack.test2; // import문을 사용하는 경우

import practicepack.test.ExampleImp // import문 작성

public class PackageImp {
		public static void main(String[] args) {
			ExampleImp x = new ExampleImp(); // 이제 패키지명을 생략 가능
		}
}

코드가 짧아지고 깔끔해진다.

접근 제어자

제어자(Modifier)

자바 프로그래밍에서 제어자는 클래스, 필드, 메서드, 생성자 등에 부가적인 의미를 부여하는 키워드를 의미한다. 명사를 꾸며주는 형용사의 역할과 같다고 할 수 있다.
자바에서 제어자는 크게 접근 제어자와 기타 제어자로 구분 할 수 있다.

중요)
하나의 대상에 대해서 여러 제어자를 사용할 수 있다.
하지만, 각 대상에 대해서 접근 제어자는 단 한번만 사용할 수 있다.**

static, final, abstract 키워드가 주로 사용된다.

접근 제어자(Access Modifier)

접근 제어자를 사용하면 클래스 외부로의 불필요한 데이터 노출을 방지(data hiding)할 수 있고, 외부로부터 데이터가 임의로 변경되지 않도록 막을 수 있습니다.

예시)

package package1; // 패키지명 package1 

//파일명: Parent.java
class Test { // Test 클래스의 접근 제어자는 default
    public static void main(String[] args) {
        Parent p = new Parent();

//        System.out.println(p.a); // 동일 클래스가 아니기 때문에 에러발생!
        System.out.println(p.b);
        System.out.println(p.c);
        System.out.println(p.d);
    }
}

public class Parent { // Parent 클래스의 접근 제어자는 public
    private int a = 1; // a,b,c,d에 각각 private, default, protected, public 접근제어자 지정
    int b = 2;
    protected int c = 3;
    public int d = 4;

    public void printEach() { // 동일 클래스이기 때문에 에러발생하지 않음
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
    }
}

// 출력값
2
3
4

private 가 있는 a 는 같은 클래스가 아니기 때문에 에러가 난다.

다른패키지를 만든 예시)

package package2; // package2 

//파일명 Test2.java
import package1.Parent;

class Child extends package1.Parent {  // package1으로부터 Parent 클래스를 상속
    public void printEach() {
        // System.out.println(a); // 에러 발생!
        // System.out.println(b);
        System.out.println(c); // 다른 패키지의 하위 클래스
        System.out.println(d);
    }
}

public class Test2 {
    public static void main(String[] args) {
        Parent p = new Parent();

//        System.out.println(p.a); // public을 제외한 모든 호출 에러!
//        System.out.println(p.b);
//        System.out.println(p.c);
        System.out.println(p.d);
    }
}

Child는 package1의 Parent를 상속 받고 있다.
private과 default a, b 는 다른 패키지에서 사용 할 수 없고, protected 는 상속받은 클래스 이기때문에 사용가능하다. 알다시피, public은 모두 사용할 수 있다.

위 예시에서 아래 Test2클래스는 상속받은 클래스도 없기 때문에 public 빼고는 모두 에러가 난다.

getter와 setter 메서드

객체지향의 캡슐화의 목적을 달성하면서도 데이터의 변경이 필요한 경우는 어떻게 할 수 있을까?
대표적으로 private 접근제어자가 포함되어 있는 객체의 변수의 데이터 값을 추가하거나 수정하고 싶을 때를 생각해볼 수 있다.

이런 경우 우리는 getter와 setter 메서드를 사용할 수 있다.

예시)

public class GetterSetterTest {
    public static void main(String[] args) {
        Worker w = new Worker();
        w.setName("김코딩");
        w.setAge(30);
        w.setId(5);

        String name = w.getName();
        System.out.println("근로자의 이름은 " + name);
        int age = w.getAge();
        System.out.println("근로자의 나이는 " + age);
        int id = w.getId();
        System.out.println("근로자의 ID는 " + id);
    }
}

class Worker {
    private String name; // 변수의 은닉화. 외부로부터 접근 불가
    private int age;
    private int id;

    public String getName() { // 멤버변수의 값 
        return name;
    }

    public void setName(String name) { // 멤버변수의 값 변경
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if(age < 1) return;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

// 출력값
근로자의 이름은 김코딩
근로자의 나이는 30
근로자의 ID는 5

먼저 setter 메서드는 외부에서 메서드에 접근하여 조건에 맞을 경우 데이터 값을 변경 가능하게 해주고 일반적으로 메서드명에 set-을 붙여서 정의한다.

예시를 보면 이름 값을 변경하기 위해 setName()이라는 메서드를 사용하고 있음을 확인할 수 있다.

한편 getter 메서드는 이렇게 설정한 변수 값을 읽어오는 데 사용하는 메서드이다.

경우에 따라 객체 외부에서 필드 값을 사용하기에 부적절한 경우가 발생할 수 있는데 이런 경우에 그 값을 가공한 이후에 외부로 전달하는 역할을 하게 된다.

위의 예시에서 볼 수 있듯이 get-을 메서드명 앞에 붙여서 사용한다.

예시를 좀 더 자세히 살펴보면, 먼저 Worker 클래스를 기반으로 객체 인스턴스를 생성해주고 같은 타입을 가지고 있는 참조변수 w에 담았다.

다음으로 w의 setter 메서드를 사용하여 이름, 나이, 아이디에 대한 데이터 값을 저장하고, getter 메서드를 통해 해당 데이터 값을 불러와 변수에 담아 출력해주고 있다.

cmd+n / Constructor 로 getter and setter를 실행할 수 있다.

출처 codestates.

profile
성장하는사람이 되자

0개의 댓글