네트워크 캠퍼스/JAVA

240105 기본타입과 참조타입, 접근제한자와 상속, 오버라이딩과 오버로딩, this

gayeon_ 2024. 1. 11. 16:19

기본 타입(primitive type)

  • 기본 타입이란 정수, 실수, 문자, 논리 값을 저장하는 데이터 타입이다.
  • 기본 타입으로 선언된 변수는 실제 값(value)을 변수 안에 저장한다.

참조 타입(reference type)

  • 참조 타입이란 객체의 주소를 참조하는 타입으로 배열, 클래스, 인터페이스 타입이다.
  • 참조 타입으로 선언된 변수는 메모리의 주소값을 변수 안에 저장합니다.
  • 참조 타입으로 선언된 변수는 스택(stack)영역에 주소값을 저장하고 내부의 실제 값은 힙(heap)영역에 저장합니다.

 

복사의 두 가지 유형

- 얕은 복사

- 깊은 복사

 

얕은 복사

 

기본형 변수는 단순 대입으로도 값 복사가 일어나지만

 

int a = 5;

int b = a;// b에 a변수에 들어있던 값인 5 대입

 

기본형 변수의 값 복사

 

 

참조형 변수는 주소값만 복사가 일어나고 실제 힙 영역에 있는 자료는 복사되지 않는다.

 

int[] a = {1, 2, 3, 4, 5}

int[] b = a;// a가 저장중인 주소값 복사가 일어남.

 

참조형 변수의 얕은 복사

참조형 변수 간 대입연산자 사용시 주소값만 복사하는 행위를 얕은 복사라고 한다.

 

 

얕은복사가 이뤄지면 같은 자료를 두 개의 변수가 동시에 조회하게 된다.

a의 배열에서 하나의 배열 값을 변경했을때

a[0] = 99;

두 변수가 모두 영향을 받는 문제가 발생한다.

 

 

깊은 복사

참조형 변수간 복사는

주소값이 아닌 해당 주소로 접근했을때 조회되는 자료를 복사한다.

 

깊은복사가 수행될때는

힙에 실제 자료를 하나 더 복사대상과 똑같이 할당한 다음, 새롭게 할당된 자료의 주소를 변수에 대입한다.

 

int[] a = {1, 2, 3};

int[] b = a.clone(); //.clone(), .copy() 등을 보편적으로 깊은복사용으로 제공합니다.

 

 

a[0] = 99;

//b[0]는 여전히 1 유지.

 

 

깊은 복사는 하나의 자료의 값을 변경해도 다른 하나는 영향을 받지 않는다.

 

 

equals()

  • 참조 타입 String과 객체 동등 비교 메서드 equals()
  • 자바는 문자열이 동일하다면 String 객체를 공유하도록 되어있다. 그래서 단순히 문자열을 String 변수에 할당한다면 같은 주소값을 갖게 된다.

ex) String str1 = "Hello"; String str2 = "Hello"; --> str1 == str2 -> true

  • 그러나 new키워드를 사용해서 String객체를 직접 heap영역에 생성한다면 문자열의 내용이 같더라도 다른 주소값을 가지게 되므로 동등, 비동등 연산자(==, !=)의 결과가 false로 나오게 된다.

ex) String str3 = new String("Hello"); String str4 = new String("Hello");

  --> str3 == str4 -> false
  • 그래서 동일 String객체이든 다른 String 객체이든 상관없이 문자열의 내용 값 그자체를 비교할 때는 equals() 메서드를 사용해야 한다.

 

- 참조 타입 String과 객체 동등 비교 메서드 equals()
- 자바는 문자열이 동일하다면 String 객체를 공유하도록 되어있다.
그래서 단순히 문자열을 String 변수에 할당한다면 같은 주소값을 갖게 된다.

ex) String str1 = "Hello";
      String str2 = "Hello";
      --> str1 == str2 -> true

- 그러나 **new키워드**를 사용해서 String객체를 직접 **heap영역**에 생성한다면 문자열의 내용이 같더라도 다른 주소값을 가지게 되므로 동등, 비동등 연산자(==, !=)의 결과가 false로 나오게 된다.

ex) String str3 = new String("Hello");
      String str4 = new String("Hello");

      --> str3 == str4 -> false

- 그래서 동일 String객체이든 다른 String 객체이든 상관없이 **문자열의 내용 값 그자체를 비교할 때**는 equals() 메서드를 사용해야 합니다.

 

 

위 코드를 실행한 결과는 다음과 같다.

 

 

배열은 참조형 변수이기 때문에 배열의 이름을 출력하면 주소가 조회된다.

실제 자료를 출력하려면 Arrays.toString()을 사용해야 한다.

 

배열 얕은 복사 예제

// 새로 생성하지 않고 intArray1을 대입받는 intArray2
		// 얕은 복사 예제
		int[] intArray2 = intArray1;
		
		intArray1[0] = 100;
		System.out.println(Arrays.toString(intArray1));
		System.out.println(Arrays.toString(intArray2));
		System.out.println("배열 2의 주소: " + intArray2);

 

디버깅으로 같은 주소를 할당 받는 것을 확인할 수 있다.

 

 

배열 1의 첫 배열 값을 100으로 수정하니 배열 2의 값도 변경되었다.

 

 

메모리 구조는 다음과 같다.

 

 

배열 깊은 복사 예제

// 힙에 저장된 자료를 새로 똑같이 할당하는 복사를 깊은복사라고 한다.
// 변수 뒤에 .clone()을 써서 수행한다.
int[] intArray2 = intArray1.clone();
		intArray1[0] = 100;
		System.out.println(Arrays.toString(intArray1));
		System.out.println(Arrays.toString(intArray2));
		System.out.println("배열 2의 주소: " + intArray2);

.clone()을 사용하면 깊은 복사를 사용할 수 있다.

 

 

값의 수정도 배열 1에서만 일어나게 된다.

 

intArray1[0]의 값을 100으로 수정한 후의 메모리 구조는 위와 같다.

 

 

동등성 예제

package equals.str;

public class UserMain {

	public static void main(String[] args) {
		// 같은 클래스 안에서 동일한 문자열을 직접 대입하는 형식으로
		// String 객체를 생성할 시 같은 주소값을 공유하게 된다.
		
		String s1 = "안녕";
		String s2 = "안녕";
		System.out.println(s1 == s2);

	}

}

자바에서는 문자열이 동일하다면 String 객체를 공유하도록 되어있기 때문에 주소값이 동일해 true라는 결과가 나온다.

 

 

package equals.str;

public class UserMain {

	public static void main(String[] args) {
		// 같은 클래스 안에서 동일한 문자열을 직접 대입하는 형식으로
		// String 객체를 생성할 시 같은 주소값을 공유하게 된다.
		
		String s1 = "안녕";
		String s2 = "안녕";
		System.out.println(s1 == s2);
		
		String s3 = new String("안녕");
		
		// s1, s2, s3 모두가 "안녕"이라는 문자열을 가지고 있다.
		System.out.println(s1 + s2 + s3);
		
		// 그렇지만 s1과 s3는 같은 주소를 공유하지 않는다.
		System.out.println(s1 == s3);
		

	}

}

new키워드로 String 객체를 직접 힙에 생성하면 s1, s2와 같은 문자열을 갖고 있더라도 주소값이 다르기 때문에 동등 연산자의 결과가 false로 나온다.

동등 연산자는 주소를 비교하기 때문이다.

 

 

    // 문자열도 참조형 변수이므로 단순 비교는 주소값 비교만 한다.
		// 따라서 주소가 아닌 자료의 동등성을 따질 때는 .equals()를 사용한다.
		System.out.println(s1.equals(s3));

주소값 비교가 자료가 동등한지 확인하고 싶을 때는 .equals()를 사용한다.

s1과 s3의 문자열은 모두 “안녕”으로 동일하므로 위 코드의 결과는 true이다.

 

 

.clone() 동등성 예제

int[] intArray2 = intArray1.clone();
		// 이 시점에서는 배열간의 배열 값들이 같지만 주소값이 다르기 때문에 false가 나온다.
		System.out.println("배열1, 배열2간의 동등성 비교: " + (intArray1 == intArray2));

.clone()으로 깊은 복사를 해 배열1, 2의 주소값이 다르기 때문에 false이다.

 

 

접근제한자와 상속

객체 지향 프로그래밍 기술

  • OOP 기술에는 은닉(캡슐화:Encapsulation), 상속(Inheritance), 다형성(Polymorphism)이 있습니다.

클래스의 상속

  • 기본적으로 이를 위해 부모 클래스(상속해주는 입장)와 자식 클래스(상속받는 입장)가 나뉘고 상속 문법은 언어별로 다르다.
  • 현실의 상속과는 달리 자식이 부모를 지정해 상속받게 된다.
  • 클래스의 상속은 다른 클래스를 이용해 확장된 클래스를 이끌어내는 것을 의미한다.

 

학생과 직장인 클래스를 만든다고 가정하면 각각의 클래스는 사람이라면 가져야 할

공통적인 속성인 “이름”, “나이”를 가지고, 거기에 더해 학생은 “전공” 이라는 학생만의 특성을 갖고 직장인은 “월급” 이라는 직장인만의 특성을 가지게 된다.

이 경우에는 상속을 사용하지 않으면 이름과 나이를 중복해서 작성하는 상황이 발생한다.

 

 

중복된 부분을 “사람” 으로 분리한 구조는 위와 같다.

이렇게 되면 직장인과 학생은 사람쪽에 있던 자원을 모두 사용할 수 있고

그와 동시에 자신들만 가지고 있는 새롭게 정의된 자원을 활용할 수도 있다.


또한 새롭게 “사람”을 기반으로 새로운 클래스를 정의할 때도 여전히 사람에 대한 정보는 기술하지 않아도 되기 때문에 코드 확장에 좀 더 도움이 된다.


상속의 경우는 엄밀히 말하면 클래스 두 개가 구분되어 융합된 형태로 저장된다.

 

 

상속(Inheritance)

  • OOP에서 상속은 기존의 클래스를 확장하여 새로운 클래스를 이끌어내는 것을 의미한다.
  • 상속 관계는 is a 관계를 만족하는 관계이다. ex) 돌고래 is a 포유류 --> 돌고래는 포유류의 속성을 가지고 있다.
  • 상속은 기존의 코드를 재사용함으로써 불필요한 코드를 재작성하는 번거로움을 없앨 수 있고, 새로운 클래스를 만드는 시간과 노력을 줄일 수 있다.
  • 자바에서는 C++에서 사용했던 다중상속의 문제점때문에 단일상속만을 지원한다.
  • 어떤 클래스가 다른 클래스로부터 상속을 받아 만들어지면 새롭게 만들어진 클래스를 자식(child or sub)클래스라고 부르며, 멤버변수와 메서드를 물려준 클래스는 부모(parent or super)클래스라고 부른다.
  • 상속을 하면 부모클래스의 멤버변수와 메서드가 자식클래스에 상속이 된다. 그러나 부모클래스의 생성자는 상속이 되지 않는다.
  • 상속을 사용하는 키워드는 **extends**이다.
public class 자식클래스 extends 부모클래스 {
	//정의사항 기술
} 
  • 상속을 하더라도 부모 클래스에서 private 접근제한을 갖는 멤버변수와 메서드는 상속대상에서 제외된다.
  • 자바의 모든 클래스는 Object 클래스를 상속받고 있다. Object클래스는 자바의 최상위 클래스이다.

 

상속 예제

package inheritance;

public class Human {
	// 사람이라면 가져야 하는 특성을 기술한다.
	public String name;
	public int age;

}
package inheritance;

public class Student extends Human {
	
	// human의 특성인 name, age는 적지 않아도 자동으로 설정된다.
	
	public String major;

}
package inheritance;

public class Salaryman extends Human {
	
	public int salary;

}
package inheritance;

public class InheritanceExample {

	public static void main(String[] args) {

		Student st1 = new Student();
		st1.name = "김학생";
		st1.age = 21;
		st1.major = "기계공학";
		
		Salaryman sm1 = new Salaryman();
		sm1.name = "나직장";
		sm1.age = 35;
		sm1.salary = 8000;

	}

}

코드는 위와 같다.

 

 

메모리 구조는 위와 같다.

 

 

 

메서드 재정의(Overriding)

 

함수 오버라이딩

  • 함수 오버라이딩은 상속시 부모가 물려준 함수가 마음에 들지 않는다면
  • 같은 이름과 같은 파라미터, 그리고 같은 리턴자료형을 재정의하는것을 의미한다.
  • 함수 재정의!

 

위의 경우 당연히 “사람” 클래스가 정의되던 시점에서는 학생의 “전공” 특성까지 자기소개에서

콘솔에 찍어야 한다는 사실을 알 수 없고, 설령 알았다고 해도 사람 내부 영역에는 해당 특성이 없어 출력하는것 자체가 불가능한 상황이다.

이를 극복하기 위해서는 학생에 “자기소개()”를 다시 정의해야 한다.

그래서 부모클래스에 있던 메서드를 자식쪽에서 같은 이름으로 새롭게 선언할 수 있다.

또한 재정의를 했다는 것은 당연히 기존 함수가 마음에 들지 않았다는 의미이기 때문에

호출 우선권은 재정의된 함수쪽에 있다.

 

 

  • 메서드 재정의란 부모클래스로부터 상속받은 메서드를 자식클래스에서 행위를 바꾸거나 보완하기 위해 다시 정의해서 사용하는 것이다.
  • 이는 부모클래스에서 특별한 용도로 사용하던 메서드를 자식클래스에서 다른 용도로 사용할 때 필요하다.
  • 메서드가 자식클래스에서 재정의되었다면 자식객체를 통해 메서드를 호출했을 때 새롭게 재정의된 메서드가 호출된다.

메서드 재정의 규칙

  1. 반드시 상속을 전제로 한다.
  2. 반드시 반환 유형이 같아야 한다.
  3. 메서드 이름이 같아야 한다.
  4. 매개 변수 선언이 정확히 일치해야 한다.
  5. 접근제한자는 같거나 더 제한이 없어야 한다.(more public)

 

오버라이드 실습 예제

package override;

public class Human {
	
	public String name;
	public int age;
	
	public void 자기소개하기() {
		System.out.println("이름: " + name);
		System.out.println("나이: " + age);
	}
	
	public void 코딩하기() {
		System.out.println("일반인은 코딩을 못 합니다.");
	}

}
package override;

public class Programmer extends Human {
	
	public String duty;
	public int repoCount;
	
	// 오버라이드는 부모측 메서드와 이름, 파라미터, 리턴타입이 같게
	// 자식쪽에 다시 선언하면 된다.
	// 오버라이드된 메서드 왼쪽에는 일반 메서드와 달리 삼각형 심볼이 생긴다.
	// 오버라이드 애너테이션은 붙여도 되고 안 붙여도 된다.
	@Override
	public void 자기소개하기() {
		System.out.println("이름: " + name);
		System.out.println("나이: " + age);
		System.out.println("직무: " + duty);
		System.out.println("깃허브 레포지토리 개수: " + repoCount);
	}
	
	@Override
	public void 코딩하기() {
		System.out.println("개발자가 코딩합니다");
	}

}
package override;

public class OverrideExample {

	public static void main(String[] args) {
		// 프로그래머 클래스의 인스턴스를 생성해주세요.
		Programmer p1 = new Programmer();
		
		// 해당 인스턴스의 값을 대입해주세요.
		p1.name = "김개발";
		p1.age = 27;
		p1.duty = "백엔드";
		p1.repoCount = 45;
		
		// 자기소개와 코딩을 시켜보세요.
		p1.자기소개하기();
		p1.코딩하기();
	}

}

오버라이드는 부모측 메서드와 이름, 파라미터, 리턴타입이 같게 자식쪽에 다시 선언하면 된다.

오버라이드된 메서드 왼쪽에는 일반 메서드와 달리 삼각형 심볼이 생긴다.

오버라이드 애너테이션은 붙여도 되고 안 붙여도 된다.

 

애너테이션을 붙이는 이유

  • 메서드 이름, 파라미터, 리턴타입이 동일한지 확인할 수 있다.
  • 붙이지 않는 경우보다 우선적으로 식별할 수 있다.

 

함수 오버로딩

반면에 오버로딩은 과적재 라는 뜻에 맞게 같은 이름의 함수를 여럿 정의하는것이다.

함수 오버로딩은 요구 파라미터의 자료형이나, 개수가 달라야 한다.

 

“인사하기()” 라는 함수나 메서드를 정의할 때

분명히 이름을 인사하기로 2개 동시에 정의했지만

하나는 문자열만을 다른 하나는 문자열, 정수를 요구할 경우 이것을 허용하는것이 오버로딩이다.

오버로딩을 하면 사용자는 하나의 함수에 대해 여러 형태의 호출방식을 취할 수 있게 된다.

 

 

 

인사하기(문자열) 을 호출하는 예시

 

 

 

 

인사하기(문자열, 정수)를 호출하는 예시

호출 명칭이 동일하지만 어떤 메서드를 호출하는지 정확히 알 수 있다.


중복(Overloading)

  • 자바는 메서드나 생성자의 중복 선언을 허용한다.
  • 중복은 메서드 또는 생성자를 선언할 때 이름은 같지만 매개 변수의 유형이나 개수를 다르게 선언해 놓는 것을 의미한다.
  • 중복을 사용하면 하나의 메서드로 매개 변수의 유형에 따라 다른 동작이 실행되게 한다.

 

중복의 조건

  1. 이름이 같아야 한다.
  2. 접근제한자나 반환유형은 영향을 미치지 않는다.
  3. 매개 변수의 유형이 달라야 한다.
  4. 매개 변수의 개수가 달라야 한다.
  5. 매개 변수의 순서가 달라야 한다.
    1. 안녕(정수, 문자열), 안녕(문자열, 정수)는 오버로딩의 예시이다.

 

💡 실습 예제 설명

생성자를 오버로딩해보겠습니다.

생성자 오버로딩은 아무것도 입력 안 하면 0, “null” 대신 기본값이 대입되도록

전체 필드 자료가 입력되면 입력된 자료로 초기화하도록 작성해주세요

example.overload.Human 클래스 기준으로 진행해주시면 됩니다.

 

 

오버로드 실습 예제1

package overload;

public class Human {
	
	public String name;
	public int age;
	
	// 생성자 오버로딩은 생성자를 여러 유형으로 만드는 것
	// 파라미터는 () -> void 파라미터
	//         (String, int)
	// 두가지 유형으로 만들 것이다.
	public Human() { // void 파라미터 (new Human()인 경우 호출)
		name = "이름을 입력하지 않았습니다.";
		age = 20;
	}
	
	public Human(String n, int a) { // String, int 파라미터
		name = n;                   // (new Human("문자", 정수)인 경우 호출)
		age = a;
	}

}
package overload;

public class HumanMain {

	public static void main(String[] args) {
		// Human을 생성하되 하나는 void 생성자로
		// 다른 하나는 (Stirng, int) 생성자를 사용하기
		Human h1 = new Human();
		System.out.println("사람의 이름: " + h1.name);
		System.out.println("사람의 나이: " + h1.age);
		
		Human h2 = new Human("춘식이", 5);
		System.out.println("사람의 이름: " + h2.name);
		System.out.println("사람의 나이: " + h2.age);

	}

}

 

 

💡 실습 예제 설명

생성자와 일반 메서드를 모두 오버로딩해보겠습니다.

생성자는 아무것도 입력 안 하면 0, “null”등의 대신 다른 기본 값이 대입되고

String을 입력하면 “지금부터 이 아이의 이름은 XX입니다” 라고 나오게 해 주세요.

고양이는 품종, 이름을 가집니다.

call 메서드를 이용해 고양이를 부를 수 있는데

void파라미터인 call메서드 경우 “야옹아 이리와” 라고 나오고

String이 입력된 파라미터인 call 메서드인 경우 XX아 이리와 라고 출력해주세요.

example.overload.Cat 클래스 기준으로 진행해주세요.

 

오버로드 실습 예제2

package overload;

public class Cat {
	
	public String name;
	public String kind;
	
	// void 생성자
	public Cat() {
		name = "이름을 입력하지 않았습니다.";
		kind = "품종이 확인되지 않았습니다.";
		
	}
	
	// String, String 생성자 (생성자 오버로딩)
	public Cat(String n, String k) {
		name = n;
		kind = k;
		System.out.println("지금부터 이 아이의 이름은 " + name + "입니다.");
	}
	
	// void 메서드
	public void Call() {
		System.out.println(name + "아 이리와");
	}
	
	// String 메서드 (메서드 오버로딩)
	public void Call(String callName) {
		System.out.println(callName + "아 이리와");
	}

}
package overload;

public class CatMain {
	public static void main(String[] args) {
		// void 생성자로 생성한 고양이
		Cat c1 = new Cat();
		
		System.out.println(c1.name);
		System.out.println(c1.kind);
		
		c1.Call();
		c1.Call("야옹이");
		System.out.println("-------------------");
		
		Cat c2 = new Cat("춘식", "코숏");
		
		System.out.println(c2.name);
		System.out.println(c2.kind);
		
		c2.Call();
		c2.Call("춘식");
		System.out.println("-------------------");
	}

}

 


this

  • this는 자기 자신 객체를 지정할 때 사용하는 키워드이다.
  • this. 을 사용하면 동일 클래스 내의 멤버(멤버변수, 메서드)를 참조할 수 있다.
  • this()를 사용하면 생성자 내부에서 자신의 다른 생성자를 호출할 수 있다.

 

this 실습 예제

package this_;

public class Car {
	public String model;
	public int speed;
	
	public Car(String model) {
		this.model = model; // this.가 붙는다면 멤버변수
		this.speed = 0; // 굳이 this를 안 붙여도 되지만 가독성을 위해 붙임
	}
	
	public void accel() {
		if(this.speed + 10 >= 150) {
			this.speed = 150; // 최대속력 150
		} else {
			this.speed += 10;
		}
	}
	
	public void showStatus() {
		System.out.println("모델명: " + model);
		System.out.println("현재속도: " + speed);
	}
}
package this_;

public class CarMain {

	public static void main(String[] args) {
		// 자동차 2대를 만들어보자
		Car myCar = new Car("멋진차");
		Car yourCar = new Car("차차");
		
		for(int i = 0; i < 20; i++) {
			myCar.accel();
		}
		myCar.showStatus();
		
		yourCar.accel();
		yourCar.showStatus();

	}

}

this는 그 당시에 호출했던 객체의 주소를 갖게 된다.

 

 

 

여기서 this의 주소는 myCar(id=24)의 주소를 갖는다.

초록색의 model이 필드이고 회색의 model은 지역변수이며

이 지역변수는 필드에게 값을 넘겨주고 사라진다.

메모리 구조는 다음과 같다.