Information Security Study
240105 기본타입과 참조타입, 접근제한자와 상속, 오버라이딩과 오버로딩, this 본문
기본 타입(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)
함수 오버라이딩
- 함수 오버라이딩은 상속시 부모가 물려준 함수가 마음에 들지 않는다면
- 같은 이름과 같은 파라미터, 그리고 같은 리턴자료형을 재정의하는것을 의미한다.
- 함수 재정의!
위의 경우 당연히 “사람” 클래스가 정의되던 시점에서는 학생의 “전공” 특성까지 자기소개에서
콘솔에 찍어야 한다는 사실을 알 수 없고, 설령 알았다고 해도 사람 내부 영역에는 해당 특성이 없어 출력하는것 자체가 불가능한 상황이다.
이를 극복하기 위해서는 학생에 “자기소개()”를 다시 정의해야 한다.
그래서 부모클래스에 있던 메서드를 자식쪽에서 같은 이름으로 새롭게 선언할 수 있다.
또한 재정의를 했다는 것은 당연히 기존 함수가 마음에 들지 않았다는 의미이기 때문에
호출 우선권은 재정의된 함수쪽에 있다.
- 메서드 재정의란 부모클래스로부터 상속받은 메서드를 자식클래스에서 행위를 바꾸거나 보완하기 위해 다시 정의해서 사용하는 것이다.
- 이는 부모클래스에서 특별한 용도로 사용하던 메서드를 자식클래스에서 다른 용도로 사용할 때 필요하다.
- 메서드가 자식클래스에서 재정의되었다면 자식객체를 통해 메서드를 호출했을 때 새롭게 재정의된 메서드가 호출된다.
메서드 재정의 규칙
- 반드시 상속을 전제로 한다.
- 반드시 반환 유형이 같아야 한다.
- 메서드 이름이 같아야 한다.
- 매개 변수 선언이 정확히 일치해야 한다.
- 접근제한자는 같거나 더 제한이 없어야 한다.(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)
- 자바는 메서드나 생성자의 중복 선언을 허용한다.
- 중복은 메서드 또는 생성자를 선언할 때 이름은 같지만 매개 변수의 유형이나 개수를 다르게 선언해 놓는 것을 의미한다.
- 중복을 사용하면 하나의 메서드로 매개 변수의 유형에 따라 다른 동작이 실행되게 한다.
중복의 조건
- 이름이 같아야 한다.
- 접근제한자나 반환유형은 영향을 미치지 않는다.
- 매개 변수의 유형이 달라야 한다.
- 매개 변수의 개수가 달라야 한다.
- 매개 변수의 순서가 달라야 한다.
- 안녕(정수, 문자열), 안녕(문자열, 정수)는 오버로딩의 예시이다.
💡 실습 예제 설명
생성자를 오버로딩해보겠습니다.
생성자 오버로딩은 아무것도 입력 안 하면 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은 지역변수이며
이 지역변수는 필드에게 값을 넘겨주고 사라진다.
메모리 구조는 다음과 같다.
'네트워크 캠퍼스 > JAVA' 카테고리의 다른 글
240109 다형성의 개요와 다형성이 없는 예제 실습 (0) | 2024.01.12 |
---|---|
240108 this(), super(), 접근 제한자, 캡슐화 (0) | 2024.01.11 |
240104 클래스 예제, 패키지 예제 (0) | 2024.01.11 |
240103 함수, 클래스 (0) | 2024.01.11 |
240102 Eclips 환경 구축, 자바 기본 사항, 주석문과 식별자 (1) | 2024.01.11 |