-
<메소드 오버라이딩과 필드 오버라이딩>
-
<Object>
-
<각 메소드의 타입별 형변환>
-
❓instanceof 사용법
-
<Object의 toString()>
-
<Object의 equals()>
-
<Object의 hashCode()>
-
<불변객체>
-
<String클래스 = 대표적인 불변클래스>
-
❓String이 불변객체인 이유
-
<String클래스의 concat() 메소드>
-
<불변클래스 String의 다양한 메소드>
-
- charAt()
-
- toUpperCase() / toLowerCase()
-
- trim()
-
- substring()
-
<추상클래스 abstract>
-
<추상메소드 구조>
-
▶️<실습 - 도형 면적 구하기>
-
▶️<실습 - 추상클래스로 카페 시스템 만들기>
-
<final 키워드>
-
<final 클래스>
-
<final 메소드>
🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [7]일차
🚀 7일차에서는 Object클래스에 대해서 자세히 배우면서 관련된 메소드인
toString(), equals(), 그 외 메소드들을 알 수 있었다!
추상클래스와 final키워드에 대해서도 배우면서 추상클래스를 언제 사용해야하는지,
final의 역할은 무엇인지 구별할 수는 수업이었다!
<메소드 오버라이딩과 필드 오버라이딩>
// 메소드 오버로딩의 예제
public class Pen {
// 필드 오버라이딩의 대상 (color)
String color = "부모의 펜 색깔 : BLACK"; // 부모는 검은색
public void write(){
System.out.println("Pen이 씁니다.");
}
public void write(String color){}
public void write(String color, String penName){}
public void write(String color, String penName, String msg){}
public void print(){
System.out.println("a");
System.out.println("b");
}
}
// 메소드 오버라이딩의 예제
public class BallPen extends Pen{
// 필드 오버라이딩의 예제
String color = "자식의 펜 색깔 : PURPLE"; // 자식은 보라색
@Override
public void write() {
// 메소드 오버라이딩은 자식의 것을 실행
// 따라서 여기에 붙은 오버라이딩된 필드는 가장 가까운 본인의 것을 쓰게된다. = "자식의 펜 색깔 : PURPLE"
System.out.println("BallPen이 씁니다." + color);
}
// super키워드 예제
// 공통적으로 실행하고 싶었던 부모의 a,b가 실행되고,
// 본인의 c, d, e가 실행된다.
// 공통된 것들을 부모 메소드로 처리해주어 코드가 간편화되었다.
@Override
public void print() {
super.print();
System.out.println("c");
System.out.println("d");
System.out.println("e");
}
public static void main(String[] args) {
BallPen bp = new BallPen();
// 상속의 예제
bp.write();
bp.write("RED");
bp.write("BLUE", "모나미");
bp.write("GREEN", "모닝글로리", "안녕하세요");
// 메소드 오버라이딩의 다양한 예제
Pen p = new Pen();
p.write(); // 부모 인스턴스이므로, 부모의 메소드를 실행
Pen penBallPen = new BallPen();
penBallPen.write(); // 타입은 부모이지만, 메소드 오버라이딩은 자식의 것을 실행
System.out.println(penBallPen.color); // 부모의 타입이기때문에 부모의 필드가 실행
}
}
- 생성자는 메소드와 비슷하지만 다른데,
➡️ 생성자는 객체가 생성될때 딱 한번만 생성
➡️ 메소드는 중간에 호출될 수도 있다. - 생성자에서 생성자 호출은 가능하지만,
다른 메소드에서 생성자 호출은 불가능 - 생성자는 리턴타입이 있으면 안된다.
- 생성자는 클래스와 이름이 같아야한다.
public class BallPen extends Pen{
public BallPen(){
...
}
}
리턴타입이 있을 경우에는 생성자가 아닌 메소드이다.
public class BallPen extends Pen{
public int BallPen(){
return 0;
}
}
<Object>
Object는 모든 클래스의 최상위 클래스 (=조상클래스)
상속 키워드인 extends를 쓰지 않아도 기본적으로 모든 클래스는 Object를 상속받는다
클래스를 만들고 그 클래스의 인스턴스를 만든 후 사용가능한 메소드에 접근해보면,
컴파일러가 자동으로 Object를 상속한 것과 같이 현재 사용할 수 있는 메소드들을 불러준다.
(ex. equals(), hashCode(), toString() ...)
(그 외. notify(), wait() ... )은 "스레드"시 사용되는 메소드이다.
<각 메소드의 타입별 형변환>
public class Exam01 {
public static void test(Pen pen){
}
public static void test2(BallPen ballPen){
}
public static void test3(Object obj){
if(obj instanceof Pen){ // Pen과 같을때만 write()를 실행시켜주라는 의미이다.
// Object를 상속받는 Pen의 메소드를 쓰기 위해서는 형변환을 해주어야한다.
((Pen)obj).write();
}
else{
System.out.println("Pen만 쓸 수 있습니다.");
}
}
public static void main(String[] args) {
Pen pen = new Pen();
BallPen ballPen = new BallPen();
String str = "Hello";
Object obj = new Object();
// test()는 Pen을 받아들이므로, pen과 Pen의 자손인 ballPen까지 올 수 있다.
System.out.println("[예제1] Pen타입을 받는 test()메소드-----");
test(pen);
test(ballPen); // Pen으로 묵시적 형변환
// test2()는 ballPen이므로, BallPen타입밖에 들어가지 못한다.
System.out.println("\\n[예제2] BallPen타입을 받는 test2()메소드-----");
test2(ballPen);
// test3()는 모든 객체의 최상위 클래스이므로, 모든 객체가 올 수 있다.
System.out.println("\\n[예제3] Object타입을 받는 test3()메소드-----");
test3(pen); // Object타입으로 묵시적 형변환
test3(ballPen); // Object타입으로 묵시적 형변환
test3(str); // Object타입으로 묵시적 형변환
test3(obj); // Object타입으로 묵시적 형변환
}
}
- 부모클래스가 기본생성자를 가지고 있지 않다면,
사용자는 반드시 직접 super()생성자를 호출하는 코드를 작성해야한다. - 또한 생성자는 무조건 super()생성자를 호출해야한다.
사용자가 super()생성자를 호출하는 코드를 작성하지 않았다면 자동으로 부모의 기본 생성자가 호출된다.
❓instanceof 사용법
public static void test3(Object obj){
if(obj instanceof Pen){ // Pen과 같을때만 write()를 실행시켜주라는 의미이다.
// Object를 상속받는 Pen의 메소드를 쓰기 위해서는 형변환을 해주어야한다.
((Pen)obj).write();
}
else{
System.out.println("Pen만 쓸 수 있습니다.");
}
}
위의 코드에서 test3를 보면 예를 들어 String타입 같은 경우가 매개변수로 들어오면
이 경우 형변환이 필요하지 않은 경우이므로 에러가 발생한다.
instanceof 를 사용해서 타입이 맞을때만 형변환 시켜줄 수 있다.
Object를 받아들이고 Pen타입과 같아야지만 형변환을 해주는 것이다.
<Object의 toString()>
System.out.println(cup); // 그냥 출력하게되면 의미없는 값이 나온다.
System.out.println(cup.toString()); // 이또한 똑같이 실행된다.
toString()또한 오버라이딩 되지 않으면
[Cup@2f43709 ]
처럼 의미없는 값이 나오게된다.
cup일때의 출력값과 cup.toString()의 출력값이 같다는 것은
toString()이 자동으로 호출되고 있음을 알 수 있다.
즉 toString()도 Object의 메소드이기때문에 Object가 자동으로 상속되어 불러진 것을 확인 가능하다.
class Cup{
String name;
int price;
}
class Cup extends Object{
String name;
int price;
}
<toString() 메소드 오버라이딩>
1. Generate메뉴 > Override Method 로 만들어서 본인이 직접 작성하거나
@Override
public String toString() {
return "이 컵은 " + name + "입니다.";
}
2. toString() 오버라이딩 메뉴를 선택해도 된다. 자동으로 양식이 만들어진다.
@Override
public String toString() {
return "Cup{" +
"name='" + name + '\\'' +
", price=" + price +
'}';
}
<toString() 메소드를 오버라이딩 하는 이유>
최상위 클래스 Object의 toString()을 사용하면, toString()메소드를 오버라이딩하기만해도
자신이 객체를 보여주는 방식을 정의할 수 있다.
만약 Object가 toString()을 갖고 있지 않았다면 각 객체들이 표현하는 방식이 달라서
객체를 사용하는 사용자는 일일이 다른 방식을 사용해야해서 불편했을 것이다.
<String타입의 toString()>
String str = “Hello”;
이 코드는 이미 String이 toString()메소드를 오버라이딩 해놓은 형태이다.
따라서 원하는 “Hello”의 값이 온전히 출력되는 것이다.
String API에서 확인이 가능하다.

<Object의 equals()>
toString()과 비슷하게 쓰이는 메소드로 equals()도 존재한다.
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.i = 10;
MyClass myClass2 = new MyClass();
myClass2.i = 10;
// myClass의 i에 10이 들어갔을때,
// myClass2의 i에도 10이 들어간다.
// 이렇게 실제 객체를 비교하는 것이 아니라 주소값을 비교한다.
System.out.println(myClass == myClass2);
// 위 코드를 개선하여 이 두개의 값이 같은지를 판단할 수 있는 메소드가 equals()메소드
// Object가 구현하고 있는 equals()는 실제 객체가 아닌 주소값을 비교하고 있다.
// 사용자가 equals()를 오버라이딩하여 위처럼 myClass.i = 10, myClass2.i = 10;처럼 같을때는
// equals() 메소드를 오버라이딩하여 같다고 정의내릴 수 있다.
System.out.println(myClass.equals(myClass2));
}
}
사용자가 객체를 비교할때, equals()은 각 객체의 레퍼런스 값이 다르기 때문에 주소를 비교하여
주소가 다르면 다르다고 반환한다.
equals()를 오버라이딩하여 판단하는 기준을 사용자가 재정의 할 수 있는 것이다.

<오버라이딩 된 equals()>
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return i == myClass.i;
}
System.out.println("myClass와 myClass2의 equals() 비교결과 : " + myClass.equals(myClass2));
- myClass.equals()는 myClass2를 인자로 보낸다.
- equals()에서는 무엇을 받는지 모르기때문에 Object타입으로 형변환되어 o로 들어온다.
- if (this == o)
this가 myClass, o는 myClass2를 의미한다. - getClass() != o.getClass()
instanceof와 같은 의미이다.
- getClass로 할지, instanceof로 할지는 IntelliJ에서 Generate equals()를 할때 체크표시를 선택해주면 된다.
- if( ! (o instanceof MyClass myClass) )
: myClass.equals(”str형이다”)일때는 String타입이어서 MyClass타입이 아니므로 형이 같지 않아
비교를 하지 않을 것이라는 의미이다.
이렇게 instanceof로 인스턴스의 타입을 비교하는 것이다. - o == null
들어온 값 o가 null이면 비교를 하지 않고 바로 false를 리턴

정리
- 각 MyClass타입을 참조하는 레퍼런스 변수가 가지고 있는 i값이 같으면 같다고 판단하고 싶을때는
equals()메소드를 오버라이딩
모든 필드가 같지 않아도 특정 필드만 같아도 같다고 표현하는 것도 가능하다.
<equals() 메소드 오버라이딩> (이름과 나이만)
// 이름과 나이가 같으면 true를 리턴하도록 함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// 각 MyClass타입을 참조하는 레퍼런스 변수가 가지고 있는 i값이 같으면 같다고 판단하고 싶을때
// equals()메소드를 오버라이딩
// 오버라이딩 후에는 true가 나오는 것을 확인할 수 있다.
Person kim = new Person("김자바", 20, "강남구 역삼동");
Person kim2 = new Person("김자바", 20, "강서구 염창동");
// 주소는 같지 않지만 Person에 equals()를 오버라이딩했을때 이름, 나이만 비교하도록 했으므로
// true가 반환된다.
System.out.println("kim과 kim2의 비교결과 : " + kim.equals(kim2));

주소가 달라도 이름과 나이가 같으면 같다고 판단하여 true를 리턴하는 것이다.
<Object의 hashCode()>
해시코드는 "객체의 값들을 숫자로 표현해놓은 것"
즉, hashCode()는 어떤객체에 대해서 숫자로 값을 리턴하는 메소드이다.
// hashCode() 예제
Person kim3 = new Person("김자바", 30, "강서구 염창동");
System.out.println("kim과 kim2의 hashCode 비교결과 : (kim = "
+ kim.hashCode() + ", kim2 = " + kim2.hashCode() + ")");
// 이름과 나이가 같을때 해시코드가 같지만, 나이를 변경한 kim3와 비교했을때의 hashCode값
System.out.println("kim과 kim3의 hashCode 비교결과 : (kim = "
+ kim.hashCode() + ", kim3 = " + kim3.hashCode() + ")");

두 메모리가 다르지만, 해시코드가 같은 값을 리턴하는 것을 확인할 수 있다.
메소드 오버라이딩 시 이름과 나이가 같을때 같다고 기준을 만들었기때문에
나이를 변경하면 kim과 kim3의 비교결과처럼 해시코드 값이 달라짐을 확인할 수 있다.
<Object가 정의한 메소드들의 필요성>
- Object가 정의한 메소드대로 사용하기엔 불편할 수 있다.
- Object도 최소한만 정의해놓고, 사용자가 이 toString()이나 equals()메소드를 오버라이딩하여 사용한다.
- Object가 의미없이 구현했지만 표준을 만들어놓았기에 사용자가 같은 방법으로 사용할 수 있게된다.
<현실세계에서의 표준 예시>
데이터베이스를 보면 각 데이터베이스마다 표준이 있다.
💡데이터베이스 종류가 많음에도 표준인 SQL이 있기때문에 기본적인 query문들은 같은 기준을 쓰는 것을 확인가능하다.
<불변객체>
<String클래스 = 대표적인 불변클래스>
String str1 = “hello”;
이처럼 String을 가장 간단하게 사용할 수 있는데
String은 기본데이터타입이 아닌 레퍼런스타입이자, 자바의 객체이다. (String클래스)
String은 객체여서 new키워드를 통해 인스턴스화해야하지만, 바로 값을 넣는 방식도 가능한 것을 확인 가능하다.
String str2 = new String(”hello”);
물론 String API에서 String의 생성자를 가보면 생성자가 존재하므로, new키워드를 사용해서 생성할 수도 있다.


String original, StringBuffer buffer, StringBuilder builder 를 통해서 만들어내기도 한다는 것을 알 수 있다.
이 클래스들을 사용하는 이유는 String을 쓰다보면 값이 많이쌓여서 메모리가 과도해질수있는데
StringBuffer나 StringBuilder와 같은 불변클래스가 아닌 클래스로 String문자열을 사용하면 해결할 수 있다.
이처럼 String클래스는 메모리가 많이 사용되므로 String값만 저장하는 메모리가 따로 존재한다.
1) 먼저 String이 사용될때,
String str1 = “hello”;
- 이 메모리 영역에 "hello"라는 문자열이 있는지 먼저 검사한다.
- 없다면 "hello"를 만들게되고, str1이 "hello"를 가리키도록 한다. (str1은 참조변수이자, 레퍼런스 변수)
String str2 = “hello”;
- str2이 만들어질때도 "hello" String 메모리 영역에서 검사한 후
- str1에서 "hello"를 만들었기때문에
- str2도 이 같은 값인 "hello"를 가리키게 된다.
2) new키워드를 사용하여 String 객체를 만들때
위에서 처럼 String값만 저장하는 메모리에 저장되는 것이 아닌 따로 String객체를 만들게 된다.
따로 인스턴스를 만들어 그 인스턴스를 가리키게 되는 것이다.
// 일반 선언
String str1 = "hello";
String str2 = "hello";
// new 선언
String str3 = new String("hello");
String str4 = new String("hello");
// 1) 일반과 일반 비교 = 같은 값을 가리키고 있으므로 = true
System.out.println("일반 String 비교 = " + (str1 == str2));
// 2) 일반과 new 비교 = 주소값만 비교하므로 다름 = false
System.out.println("일반 String, new String 비교 = " + (str1 == str3));
// 3) new와 new 비교 = 다른 인스턴스이므로 다른 주소값을 가리킨다. = false
System.out.println("new String 비교 = " + (str3 == str4));

일반 String과 new로 선언한 String이 같은지 확인해보면 false로 다른것을 확인할 수 있다.
이처럼 String이 메모리 상에 별도의 메모리 공간을 갖는 이유는
"너무 많은 문자열이 생성되므로, 효율적으로 관리할 방법을 제공"하는 것
new키워드를 사용하는 경우는 byte배열의 객체나 StringBuilder등을 이용해 만들고 싶을때는 사용하게된다.
이후로도 "hello"를 계속 만들게되어도
String str11 = “hello”;
String str22 = “hello”;
- 참조변수(레퍼런스변수)가 여러개 선언되어도
- String에서 문자열을 관리하는 공간에는 “hello”하나만 저장되어있고,
- 그 많은 레퍼런스 변수들은 다 이 하나의 “hello”만 가리키게 되는 것이다.
<equals()를 쓰지 않고 == 를 써도 올바른 결과가 출력되는 이유>
String클래스가 메모리 상에 문자열을 따로 관리를 하기 때문에
객체를 비교하지 않아도 같은 문자열을 가리키므로 true로 출력될 수 있다.
❓String이 불변객체인 이유
public class StringExam02 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
MyClass myClass3 = myClass;
myClass.i = 10;
myClass2.i = 20;
myClass3.i = 30;
// 같은 것을 가리키고 있었으므로 myClass3에서 i를 바꾸더라도 값이 같이 바뀌게 된다.
// 따라서 마지막에 바꿔준 30으로 모두 변경되어있다.
System.out.println("myClass.i : " + myClass.i);
System.out.println("myClass2.i : " + myClass2.i);
System.out.println("myClass3.i : " + myClass3.i);
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
str1 = "HelloJava";
// 불변클래스이기때문에 str1만 "HelloJava"로 바뀌는 것을 확인할 수 있다.
System.out.println("str1 : " + str1);
System.out.println("str2 : " + str2);
System.out.println("str3 : " + str3);
}
}
- String이 아래와 같이 생성되면 hello라는 문자열은 하나만 만들어진다.
- 모두 같은 문자열을 가리키고 있다. "hello"
- 하나를 바꿨을때, 모두 바뀌면 안되기때문에 String은 한번 생성되면 스스로 바뀌지 않는 "불변 클래스"이다.
<String클래스의 concat() 메소드>

이 메소드는 문자열 끝에 추가로 붙이는 기능을한다.
System.out.println(str3.concat("추가적인 concat"));
System.out.println(str3);
concat()으로 str3에 변형을 주어도 str3을 다시 출력해보면 그대로 “hello”가 출력되는 것을 확인할 수 있다.
<MyClass> - concat() 메소드 만들기
// 불변 클래스 String 예제를 위한 메소드
public int concat(int j){
i += j;
return i;
}
<StringExam02>
// i의 값이 실제로 바뀐 것을 확인할 수 있다.
System.out.println(myClass.concat(10));
System.out.println(myClass.i);
- 예제를 실행해보면 i의 값이 실제로 바뀌는데,
- 이는 불변 클래스 String이 concat()을 사용했을때 값이 바뀌지 않는 것 과는 다른 결과이다.
- String의 값을 바꿔주고 싶으면
str1 = str1.concat("String의 concat입니다.");
// 주소를 바꿔줘야한다.
<String이 불변클래스일 수 밖에 없는 이유>
concat()과 같은 메소드를 사용할때 추가되는 문자열로 인해 의도치 않은 값 변경이 일어나는 경우를 방지하기 위해
String을 참조하는 변수의 주소값을 직접 바꿔줘야한다.
str1 = str1.concat();
비슷한 예제를 살펴보면
public class ConcatStringExam {
private String name;
// 문자열 뒤에 값을 붙이는 메소드 concat을 만들어줌
public String concat(String word){
name = name + word;
return name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ConcatStringExam c1 = new ConcatStringExam();
c1.setName("김자바");
System.out.println(c1.getName());
// 1) 일반 클래스에서의 concat 예시
c1.concat("입니다.");
System.out.println(c1.getName());
System.out.println("--------------------------");
// 2) 불변 클래스에서의 concat 예시
String str1 = "김자바";
str1.concat("입니다.");
System.out.println(str1);
System.out.println("--------------------------");
// 3) concat으로 바뀐 값을 String에 적용해주고싶으면
// str1의 주소값을 새롭게 설정해주면 된다.
str1 = str1.concat("입니다.");
System.out.println(str1);
}
}
1번 예시의 경우
- name 필드 자체가 바뀌었기 때문에
- c1.getName()을 해주어도 "김자바+입니다"가 출력된다.
2번 예시의 경우
- 하지만 String은 그대로 "김자바"만 출력됨을 확인할 수 있다.
- "김자바입니다" 처럼 concat()이 적용된 문자열을 출력하고 싶으면
- str1 = str1.concat("입니다"); 로 새 주소값을 지정해주어야한다.
<불변클래스 String의 다양한 메소드>
- charAt()
: index를 입력받아, 그 인덱스에 해당하는 char타입 문자를 리턴한다.
String greeting = "Hello World!";
// 1) length() : 문자열의 길이를 리턴
System.out.println(greeting.length());
// 2) charAt(int index) (반환타입 char) : 특정 인덱스의 문자를 리턴
System.out.println(greeting.charAt(0)); // H 출력
- charAt()메소드 응용
// 2-1) 문자열 안에 'l'이 몇번 들어갔는지 확인 가능
int count = 0;
for(int i = 0; i < greeting.length(); i++){
if(greeting.charAt(i) == 'l'){
count++;
}
}
System.out.println("2-1) 일반 for문 : " + greeting + "안의 (l)의 개수 : " + count + "개");
// 2-2) 2-1을 메소드로 선언
findCount(greeting);
// 3) 문자열 안에 특정문자가 있는지 확인하는 boolean메소드
// boolean detector(String data, char findChar){}
// data안에 findChar가 존재하면 true, 없으면 false리턴
boolean result = detector(greeting, 'r');
System.out.println("3) [" + greeting + "]에서 [r]을 찾아본 결과 : " + result);
private static void findCount(String greeting) {
int count = 0;
for(int i = 0; i < greeting.length(); i++){
if(greeting.charAt(i) == 'l'){
count++;
}
}
System.out.println("2-2) findCount 메소드 : " + greeting + "안의 (l)의 개수 : " + count + "개");
}
private static boolean detector(String data, char findChar){
for(int i = 0; i < data.length(); i++){
if(data.charAt(i) == findChar) return true;
}
return false;
}
charAt() 메소드 응용에서 findCount(), detector()와 같은 메소드를 만들어서 활용해볼 수 있었다.
- toUpperCase() / toLowerCase()
: 문자열의 대소문자를 변환한다.
: String이 변하는 것이 아닌 새롭게 만들어진 문자열을 반환하는 것
// 대소문자 변환 (toUpperCase / toLowerCase)
String upper = greeting.toUpperCase();
String lower = greeting.toLowerCase();
System.out.println("대문자 변환: " + upper);
System.out.println("소문자 변환: " + lower);

- trim()
: 문자열의 공백을 제거한다.
// 공백 제거 (trim 메서드)
String spacedString = " Hello! ";
System.out.println("공백 제거 전 길이: " + spacedString.length());
String trimmed = spacedString.trim();
System.out.println("공백 제거 후 길이: " + trimmed.length());
System.out.println("trimmed: \\"" + trimmed + "\\"");

- substring()
: 문자열의 부분문자열을 추출한다.
// 부분 문자열 추출 (substring 메서드)
String substring = greeting.substring(7, 12); // "World" 추출
System.out.println("substring: " + substring);

<추상클래스 abstract>
추상클래스는 추상메소드를 포함할 수 있는 클래스
- 구체적인 인스턴스는 생성할 수 없으며, 상속을 통해서만 사용
- 공통적인 기능(필드, 메소드)를 정의하고, 일부 메소드는 “하위 클래스에서 반드시 구현하도록 “강제””
- 인스턴스화 불가 : new를 이용해 객체를 생성하지 못한다. 자식클래스로 객체를 생성해야한다.
- 상속을 통해 구현 확장 : 추상메소드를 구현하여 하위 클래스에서 “구체화”
- 구현메소드 포함 가능 : 추상클래스는 추상메소드 뿐만 아니라 일반 메소드도 포함할 수 있다.
- 필드, 생성자, 정적메소드 소유 가능 : 일반 클래스와 마찬가지로 필드, 생성자, static메소드를 포함할 수 있다.
<추상클래스 선언>
// 추상 클래스인 새 예제
public abstract class AbstractBird {
private String name;
private int age;
public void eat(){
System.out.println("새가 먹기 시작합니다.");
}
public void fly(){
System.out.println("새가 날기 시작합니다.");
}
/*
새가 어떻게 노래할까를 정의하려면,
새의 종류를 알아야하고 울음소리가 특정되지 않았으므로
추상적인 클래스라고 볼 수 있다.
"설계가 미완성"
*/
abstract public void song();
}
자식클래스들이 표준화된 방법으로 song()라는 메소드를 정의하게된다. (표준화)
자식클래스는 song()라는 메소드를 반드시 구현해야한다 (강제성)
<추상메소드 구조>
/*
새가 어떻게 노래할까를 정의하려면,
새의 종류를 알아야하고 울음소리가 특정되지 않았으므로
추상적인 클래스라고 볼 수 있다.
"설계가 미완성"
*/
public void song(){ };
추상메소드는 위와 같이 구현부가 있는 것이 아닌
public abstract void song();
처럼 구현부를 생략하고 앞에 abstract 키워드를 붙여 작성한다.
추상메소드는 하위 클래스에서 반드시 해당 메소드를 구현(Override)해야한다.
<추상클래스를 상속받은 자식클래스>
public class Crow extends AbstractBird{
@Override
public void song() {
System.out.println("추상클래스를 상속받은 [까마귀]가 노래합니다.");
}
}
// 추상클래스를 상속받은 자식클래스는 반드시 구현해야한다 (강제성)
public class Duck extends AbstractBird{
@Override
public void song() {
System.out.println("추상클래스를 상속받은 [오리]가 노래합니다.");
}
}
<추상클래스 실행 main메소드>
public static void main(String[] args) {
// 추상클래스이므로 인스턴스를 생성할 수 없다.
AbstractBird d = new Duck();
d.song();
AbstractBird c = new Crow();
c.song();
}
위에서 설명한
“Abstract class 는 추상메소드를 포함할 수 있는 클래스” 는
// 추상 클래스인 새 예제
public abstract class AbstractBird {
public abstract void song();
}
이러한 구조이다.
<Object의 equals(), toString()가 추상메소드가 아닌 이유>
먼저 추상메소드는 강제성을 가지고 있다.
그렇다면 Object를 상속받는 모든 클래스는 equals(), toString()를 반드시 구현해야하는 강제성을 가진다.
하지만 이 메소드들은 반드시 구현해야만 하는 기능은 아니었으므로, 강제성이 있는 추상메소드일 필요가 없는 것이다.
▶️<실습 - 도형 면적 구하기>
1. 추상클래스를 먼저 만든다.
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// 어떤 도형이든 면적을 계산하는 기능은 반드시 필요하므로
// 자식클래스에서 이 메소드 구현을 강제함
// 각 도형마다 면적을 구하는 방식은 다르므로, 자식클래스마다 다르게 구현되어야함
public abstract double getArea();
public String getColor() {
return color;
}
}
2. 이 추상클래스를 상속받고, 추상메소드 getArea()를 구현할 Circle클래스를 만든다.
public class Circle extends Shape{
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
3. Circle뿐만 아니라, Rectangle, Triangle 클래스도 만들어준다.
// Rectangle의 추상메소드 구현 예제
@Override
public double getArea() {
return width * height;
}
// Triangle의 추상메소드 구현 예제
@Override
public double getArea() {
return bottom * height;
}
4. 일반 출력
// 1) 인자로 값 지정해서 넣어주기
Shape c = new Circle("RED", 3);
System.out.println("원의 색깔 : " + c.getColor() + ", 원의 넓이 : " + String.format("%.1f", c.getArea()));
Shape r = new Rectangle("BLUE", 5, 10);
System.out.println("사각형의 색깔 : " + r.getColor() + ", 사각형의 넓이 : " + String.format("%.1f", r.getArea()));
Shape t = new Triangle("GREEN", 30, 3);
System.out.println("삼각형의 색깔 : " + t.getColor() + ", 삼각형의 넓이 : " + String.format("%.1f", t.getArea()));

5. Scanner로 입력받아 출력
// 2) Scanner로 사용자에게 입력받아 인자 넣어주기
Scanner sc = new Scanner(System.in);
for(int i = 0; i < 3; i++){
System.out.print("도형의 색깔을 지정 : ");
String color = sc.next();
switch (i){
case 0 : // 원을 의미
System.out.print("원의 반지름 : ");
double radius = sc.nextDouble();
Shape c2 = new Circle(color, radius);
System.out.println("원의 색깔 : " + c2.getColor() + ", 원의 넓이 : " + String.format("%.1f", c2.getArea()));
break;
case 1: // 사각형을 의미
System.out.print("사각형의 가로 : ");
double width = sc.nextDouble();
System.out.print("사각형의 세로 : ");
double height = sc.nextDouble();
Shape r2 = new Rectangle(color, width, height);
System.out.println("사각형의 색깔 : " + r2.getColor() + ", 사각형의 넓이 : " + String.format("%.1f", r2.getArea()));
break;
case 2: // 삼각형을 의미
System.out.print("삼각형의 높이 : ");
double tHeight = sc.nextDouble();
System.out.print("삼각형의 밑변 : ");
double tWidth = sc.nextDouble();
Shape t2 = new Rectangle(color, tHeight, tWidth);
System.out.println("삼각형의 색깔 : " + t2.getColor() + ", 삼각형의 넓이 : " + String.format("%.1f", t2.getArea()));
break;
}
System.out.println();
}

▶️<실습 - 추상클래스로 카페 시스템 만들기>
1.final키워드를 사용하여 final 메소드를 만든다. = 음료를 만들때 이 순서는 오버라이딩 되면 안된다는 의미이다.
// 이 메소드는 final로써 이 레시피의 순서를 오버라이딩(재정의)하지 못한다.
public final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
2. brew()와 addCondiments()는 추상메소드로 선언하고, Coffee나 Tea와 같은 자식클래스에서 오버라이딩으로 구현
public static void main(String[] args) {
Beverage c = new Coffee();
Beverage t = new Tea();
System.out.println("-----[커피] 주문 접수 : 만들기 시작-----");
c.prepareRecipe();
System.out.println("\n-----[차] 주문 접수 : 만들기 시작-----");
t.prepareRecipe();
}

<final 키워드>
- 필드에 final이 붙으면 상수 : 값을 바꿀 수 없는 숫자
- 메소드에 final이 붙으면 : 오버라이딩이 되지 않음을 의미
- 클래스에 final이 붙으면 : 더이상 상속을 금지한다는 것을 의미
<final 클래스>
- 이 클래스를 상속못한다는 것을 의미 (대를 끊는다는 것을 의미)
- Math클래스도 final로 지정되어있다.
만약 누군가가 Math가 가진 메소드를 임의대로 오버라이딩하면,
다른 사용자가 Math를 사용할때 오버라이딩된 자식의 메소드를 실행하기때문에 원치 않는 결과가 출력됨을 방지
<final 메소드>
- 오버라이딩 금지. 자식이 이 메소드를 수정해서 쓸 수가 없다.
- ⭐오버로딩은 가능하다. (메소드 이름만 같을뿐 매개변수가 다르기 때문에 다른 메소드로 취급된다)
💡 7일차에는 간단하지만 자칫 이해하기 어려울 수 있는 추상클래스에 대해 깊게 공부하였다. 처음 String클래스를 알고리즘으로 접하고 equals()와 toString()같은 메소드를 사용할때는 몰랐던 개념들과 문법을 더 자세히 알 수 있었다!
또한 final키워드를 사용하는 곳에 대해 상수만 알고있었는데 메소드와 클래스에서도 사용되면 어떠한 역할을 하는지 알 수 있게되었다. 관련 실습을 더 풀어보며 개념을 다져야겠다고 생각했다!🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_10일차_'내부클래스와 제네릭' (2) | 2024.12.13 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_8일차_'인터페이스와 예외처리' (0) | 2024.12.11 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_6일차_'상속과 메소드 오버라이딩' (1) | 2024.12.09 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_5일차_'메소드, 필드, static' (3) | 2024.12.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_4일차_'객체지향프로그래밍' (1) | 2024.12.05 |
🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [7]일차
🚀 7일차에서는 Object클래스에 대해서 자세히 배우면서 관련된 메소드인
toString(), equals(), 그 외 메소드들을 알 수 있었다!
추상클래스와 final키워드에 대해서도 배우면서 추상클래스를 언제 사용해야하는지,
final의 역할은 무엇인지 구별할 수는 수업이었다!
<메소드 오버라이딩과 필드 오버라이딩>
// 메소드 오버로딩의 예제
public class Pen {
// 필드 오버라이딩의 대상 (color)
String color = "부모의 펜 색깔 : BLACK"; // 부모는 검은색
public void write(){
System.out.println("Pen이 씁니다.");
}
public void write(String color){}
public void write(String color, String penName){}
public void write(String color, String penName, String msg){}
public void print(){
System.out.println("a");
System.out.println("b");
}
}
// 메소드 오버라이딩의 예제
public class BallPen extends Pen{
// 필드 오버라이딩의 예제
String color = "자식의 펜 색깔 : PURPLE"; // 자식은 보라색
@Override
public void write() {
// 메소드 오버라이딩은 자식의 것을 실행
// 따라서 여기에 붙은 오버라이딩된 필드는 가장 가까운 본인의 것을 쓰게된다. = "자식의 펜 색깔 : PURPLE"
System.out.println("BallPen이 씁니다." + color);
}
// super키워드 예제
// 공통적으로 실행하고 싶었던 부모의 a,b가 실행되고,
// 본인의 c, d, e가 실행된다.
// 공통된 것들을 부모 메소드로 처리해주어 코드가 간편화되었다.
@Override
public void print() {
super.print();
System.out.println("c");
System.out.println("d");
System.out.println("e");
}
public static void main(String[] args) {
BallPen bp = new BallPen();
// 상속의 예제
bp.write();
bp.write("RED");
bp.write("BLUE", "모나미");
bp.write("GREEN", "모닝글로리", "안녕하세요");
// 메소드 오버라이딩의 다양한 예제
Pen p = new Pen();
p.write(); // 부모 인스턴스이므로, 부모의 메소드를 실행
Pen penBallPen = new BallPen();
penBallPen.write(); // 타입은 부모이지만, 메소드 오버라이딩은 자식의 것을 실행
System.out.println(penBallPen.color); // 부모의 타입이기때문에 부모의 필드가 실행
}
}
- 생성자는 메소드와 비슷하지만 다른데,
➡️ 생성자는 객체가 생성될때 딱 한번만 생성
➡️ 메소드는 중간에 호출될 수도 있다. - 생성자에서 생성자 호출은 가능하지만,
다른 메소드에서 생성자 호출은 불가능 - 생성자는 리턴타입이 있으면 안된다.
- 생성자는 클래스와 이름이 같아야한다.
public class BallPen extends Pen{
public BallPen(){
...
}
}
리턴타입이 있을 경우에는 생성자가 아닌 메소드이다.
public class BallPen extends Pen{
public int BallPen(){
return 0;
}
}
<Object>
Object는 모든 클래스의 최상위 클래스 (=조상클래스)
상속 키워드인 extends를 쓰지 않아도 기본적으로 모든 클래스는 Object를 상속받는다
클래스를 만들고 그 클래스의 인스턴스를 만든 후 사용가능한 메소드에 접근해보면,
컴파일러가 자동으로 Object를 상속한 것과 같이 현재 사용할 수 있는 메소드들을 불러준다.
(ex. equals(), hashCode(), toString() ...)
(그 외. notify(), wait() ... )은 "스레드"시 사용되는 메소드이다.
<각 메소드의 타입별 형변환>
public class Exam01 {
public static void test(Pen pen){
}
public static void test2(BallPen ballPen){
}
public static void test3(Object obj){
if(obj instanceof Pen){ // Pen과 같을때만 write()를 실행시켜주라는 의미이다.
// Object를 상속받는 Pen의 메소드를 쓰기 위해서는 형변환을 해주어야한다.
((Pen)obj).write();
}
else{
System.out.println("Pen만 쓸 수 있습니다.");
}
}
public static void main(String[] args) {
Pen pen = new Pen();
BallPen ballPen = new BallPen();
String str = "Hello";
Object obj = new Object();
// test()는 Pen을 받아들이므로, pen과 Pen의 자손인 ballPen까지 올 수 있다.
System.out.println("[예제1] Pen타입을 받는 test()메소드-----");
test(pen);
test(ballPen); // Pen으로 묵시적 형변환
// test2()는 ballPen이므로, BallPen타입밖에 들어가지 못한다.
System.out.println("\\n[예제2] BallPen타입을 받는 test2()메소드-----");
test2(ballPen);
// test3()는 모든 객체의 최상위 클래스이므로, 모든 객체가 올 수 있다.
System.out.println("\\n[예제3] Object타입을 받는 test3()메소드-----");
test3(pen); // Object타입으로 묵시적 형변환
test3(ballPen); // Object타입으로 묵시적 형변환
test3(str); // Object타입으로 묵시적 형변환
test3(obj); // Object타입으로 묵시적 형변환
}
}
- 부모클래스가 기본생성자를 가지고 있지 않다면,
사용자는 반드시 직접 super()생성자를 호출하는 코드를 작성해야한다. - 또한 생성자는 무조건 super()생성자를 호출해야한다.
사용자가 super()생성자를 호출하는 코드를 작성하지 않았다면 자동으로 부모의 기본 생성자가 호출된다.
❓instanceof 사용법
public static void test3(Object obj){
if(obj instanceof Pen){ // Pen과 같을때만 write()를 실행시켜주라는 의미이다.
// Object를 상속받는 Pen의 메소드를 쓰기 위해서는 형변환을 해주어야한다.
((Pen)obj).write();
}
else{
System.out.println("Pen만 쓸 수 있습니다.");
}
}
위의 코드에서 test3를 보면 예를 들어 String타입 같은 경우가 매개변수로 들어오면
이 경우 형변환이 필요하지 않은 경우이므로 에러가 발생한다.
instanceof 를 사용해서 타입이 맞을때만 형변환 시켜줄 수 있다.
Object를 받아들이고 Pen타입과 같아야지만 형변환을 해주는 것이다.
<Object의 toString()>
System.out.println(cup); // 그냥 출력하게되면 의미없는 값이 나온다.
System.out.println(cup.toString()); // 이또한 똑같이 실행된다.
toString()또한 오버라이딩 되지 않으면
[Cup@2f43709 ]
처럼 의미없는 값이 나오게된다.
cup일때의 출력값과 cup.toString()의 출력값이 같다는 것은
toString()이 자동으로 호출되고 있음을 알 수 있다.
즉 toString()도 Object의 메소드이기때문에 Object가 자동으로 상속되어 불러진 것을 확인 가능하다.
class Cup{
String name;
int price;
}
class Cup extends Object{
String name;
int price;
}
<toString() 메소드 오버라이딩>
1. Generate메뉴 > Override Method 로 만들어서 본인이 직접 작성하거나
@Override
public String toString() {
return "이 컵은 " + name + "입니다.";
}
2. toString() 오버라이딩 메뉴를 선택해도 된다. 자동으로 양식이 만들어진다.
@Override
public String toString() {
return "Cup{" +
"name='" + name + '\\'' +
", price=" + price +
'}';
}
<toString() 메소드를 오버라이딩 하는 이유>
최상위 클래스 Object의 toString()을 사용하면, toString()메소드를 오버라이딩하기만해도
자신이 객체를 보여주는 방식을 정의할 수 있다.
만약 Object가 toString()을 갖고 있지 않았다면 각 객체들이 표현하는 방식이 달라서
객체를 사용하는 사용자는 일일이 다른 방식을 사용해야해서 불편했을 것이다.
<String타입의 toString()>
String str = “Hello”;
이 코드는 이미 String이 toString()메소드를 오버라이딩 해놓은 형태이다.
따라서 원하는 “Hello”의 값이 온전히 출력되는 것이다.
String API에서 확인이 가능하다.

<Object의 equals()>
toString()과 비슷하게 쓰이는 메소드로 equals()도 존재한다.
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.i = 10;
MyClass myClass2 = new MyClass();
myClass2.i = 10;
// myClass의 i에 10이 들어갔을때,
// myClass2의 i에도 10이 들어간다.
// 이렇게 실제 객체를 비교하는 것이 아니라 주소값을 비교한다.
System.out.println(myClass == myClass2);
// 위 코드를 개선하여 이 두개의 값이 같은지를 판단할 수 있는 메소드가 equals()메소드
// Object가 구현하고 있는 equals()는 실제 객체가 아닌 주소값을 비교하고 있다.
// 사용자가 equals()를 오버라이딩하여 위처럼 myClass.i = 10, myClass2.i = 10;처럼 같을때는
// equals() 메소드를 오버라이딩하여 같다고 정의내릴 수 있다.
System.out.println(myClass.equals(myClass2));
}
}
사용자가 객체를 비교할때, equals()은 각 객체의 레퍼런스 값이 다르기 때문에 주소를 비교하여
주소가 다르면 다르다고 반환한다.
equals()를 오버라이딩하여 판단하는 기준을 사용자가 재정의 할 수 있는 것이다.

<오버라이딩 된 equals()>
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return i == myClass.i;
}
System.out.println("myClass와 myClass2의 equals() 비교결과 : " + myClass.equals(myClass2));
- myClass.equals()는 myClass2를 인자로 보낸다.
- equals()에서는 무엇을 받는지 모르기때문에 Object타입으로 형변환되어 o로 들어온다.
- if (this == o)
this가 myClass, o는 myClass2를 의미한다. - getClass() != o.getClass()
instanceof와 같은 의미이다.
- getClass로 할지, instanceof로 할지는 IntelliJ에서 Generate equals()를 할때 체크표시를 선택해주면 된다.
- if( ! (o instanceof MyClass myClass) )
: myClass.equals(”str형이다”)일때는 String타입이어서 MyClass타입이 아니므로 형이 같지 않아
비교를 하지 않을 것이라는 의미이다.
이렇게 instanceof로 인스턴스의 타입을 비교하는 것이다. - o == null
들어온 값 o가 null이면 비교를 하지 않고 바로 false를 리턴

정리
- 각 MyClass타입을 참조하는 레퍼런스 변수가 가지고 있는 i값이 같으면 같다고 판단하고 싶을때는
equals()메소드를 오버라이딩
모든 필드가 같지 않아도 특정 필드만 같아도 같다고 표현하는 것도 가능하다.
<equals() 메소드 오버라이딩> (이름과 나이만)
// 이름과 나이가 같으면 true를 리턴하도록 함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// 각 MyClass타입을 참조하는 레퍼런스 변수가 가지고 있는 i값이 같으면 같다고 판단하고 싶을때
// equals()메소드를 오버라이딩
// 오버라이딩 후에는 true가 나오는 것을 확인할 수 있다.
Person kim = new Person("김자바", 20, "강남구 역삼동");
Person kim2 = new Person("김자바", 20, "강서구 염창동");
// 주소는 같지 않지만 Person에 equals()를 오버라이딩했을때 이름, 나이만 비교하도록 했으므로
// true가 반환된다.
System.out.println("kim과 kim2의 비교결과 : " + kim.equals(kim2));

주소가 달라도 이름과 나이가 같으면 같다고 판단하여 true를 리턴하는 것이다.
<Object의 hashCode()>
해시코드는 "객체의 값들을 숫자로 표현해놓은 것"
즉, hashCode()는 어떤객체에 대해서 숫자로 값을 리턴하는 메소드이다.
// hashCode() 예제
Person kim3 = new Person("김자바", 30, "강서구 염창동");
System.out.println("kim과 kim2의 hashCode 비교결과 : (kim = "
+ kim.hashCode() + ", kim2 = " + kim2.hashCode() + ")");
// 이름과 나이가 같을때 해시코드가 같지만, 나이를 변경한 kim3와 비교했을때의 hashCode값
System.out.println("kim과 kim3의 hashCode 비교결과 : (kim = "
+ kim.hashCode() + ", kim3 = " + kim3.hashCode() + ")");

두 메모리가 다르지만, 해시코드가 같은 값을 리턴하는 것을 확인할 수 있다.
메소드 오버라이딩 시 이름과 나이가 같을때 같다고 기준을 만들었기때문에
나이를 변경하면 kim과 kim3의 비교결과처럼 해시코드 값이 달라짐을 확인할 수 있다.
<Object가 정의한 메소드들의 필요성>
- Object가 정의한 메소드대로 사용하기엔 불편할 수 있다.
- Object도 최소한만 정의해놓고, 사용자가 이 toString()이나 equals()메소드를 오버라이딩하여 사용한다.
- Object가 의미없이 구현했지만 표준을 만들어놓았기에 사용자가 같은 방법으로 사용할 수 있게된다.
<현실세계에서의 표준 예시>
데이터베이스를 보면 각 데이터베이스마다 표준이 있다.
💡데이터베이스 종류가 많음에도 표준인 SQL이 있기때문에 기본적인 query문들은 같은 기준을 쓰는 것을 확인가능하다.
<불변객체>
<String클래스 = 대표적인 불변클래스>
String str1 = “hello”;
이처럼 String을 가장 간단하게 사용할 수 있는데
String은 기본데이터타입이 아닌 레퍼런스타입이자, 자바의 객체이다. (String클래스)
String은 객체여서 new키워드를 통해 인스턴스화해야하지만, 바로 값을 넣는 방식도 가능한 것을 확인 가능하다.
String str2 = new String(”hello”);
물론 String API에서 String의 생성자를 가보면 생성자가 존재하므로, new키워드를 사용해서 생성할 수도 있다.


String original, StringBuffer buffer, StringBuilder builder 를 통해서 만들어내기도 한다는 것을 알 수 있다.
이 클래스들을 사용하는 이유는 String을 쓰다보면 값이 많이쌓여서 메모리가 과도해질수있는데
StringBuffer나 StringBuilder와 같은 불변클래스가 아닌 클래스로 String문자열을 사용하면 해결할 수 있다.
이처럼 String클래스는 메모리가 많이 사용되므로 String값만 저장하는 메모리가 따로 존재한다.
1) 먼저 String이 사용될때,
String str1 = “hello”;
- 이 메모리 영역에 "hello"라는 문자열이 있는지 먼저 검사한다.
- 없다면 "hello"를 만들게되고, str1이 "hello"를 가리키도록 한다. (str1은 참조변수이자, 레퍼런스 변수)
String str2 = “hello”;
- str2이 만들어질때도 "hello" String 메모리 영역에서 검사한 후
- str1에서 "hello"를 만들었기때문에
- str2도 이 같은 값인 "hello"를 가리키게 된다.
2) new키워드를 사용하여 String 객체를 만들때
위에서 처럼 String값만 저장하는 메모리에 저장되는 것이 아닌 따로 String객체를 만들게 된다.
따로 인스턴스를 만들어 그 인스턴스를 가리키게 되는 것이다.
// 일반 선언
String str1 = "hello";
String str2 = "hello";
// new 선언
String str3 = new String("hello");
String str4 = new String("hello");
// 1) 일반과 일반 비교 = 같은 값을 가리키고 있으므로 = true
System.out.println("일반 String 비교 = " + (str1 == str2));
// 2) 일반과 new 비교 = 주소값만 비교하므로 다름 = false
System.out.println("일반 String, new String 비교 = " + (str1 == str3));
// 3) new와 new 비교 = 다른 인스턴스이므로 다른 주소값을 가리킨다. = false
System.out.println("new String 비교 = " + (str3 == str4));

일반 String과 new로 선언한 String이 같은지 확인해보면 false로 다른것을 확인할 수 있다.
이처럼 String이 메모리 상에 별도의 메모리 공간을 갖는 이유는
"너무 많은 문자열이 생성되므로, 효율적으로 관리할 방법을 제공"하는 것
new키워드를 사용하는 경우는 byte배열의 객체나 StringBuilder등을 이용해 만들고 싶을때는 사용하게된다.
이후로도 "hello"를 계속 만들게되어도
String str11 = “hello”;
String str22 = “hello”;
- 참조변수(레퍼런스변수)가 여러개 선언되어도
- String에서 문자열을 관리하는 공간에는 “hello”하나만 저장되어있고,
- 그 많은 레퍼런스 변수들은 다 이 하나의 “hello”만 가리키게 되는 것이다.
<equals()를 쓰지 않고 == 를 써도 올바른 결과가 출력되는 이유>
String클래스가 메모리 상에 문자열을 따로 관리를 하기 때문에
객체를 비교하지 않아도 같은 문자열을 가리키므로 true로 출력될 수 있다.
❓String이 불변객체인 이유
public class StringExam02 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
MyClass myClass3 = myClass;
myClass.i = 10;
myClass2.i = 20;
myClass3.i = 30;
// 같은 것을 가리키고 있었으므로 myClass3에서 i를 바꾸더라도 값이 같이 바뀌게 된다.
// 따라서 마지막에 바꿔준 30으로 모두 변경되어있다.
System.out.println("myClass.i : " + myClass.i);
System.out.println("myClass2.i : " + myClass2.i);
System.out.println("myClass3.i : " + myClass3.i);
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
str1 = "HelloJava";
// 불변클래스이기때문에 str1만 "HelloJava"로 바뀌는 것을 확인할 수 있다.
System.out.println("str1 : " + str1);
System.out.println("str2 : " + str2);
System.out.println("str3 : " + str3);
}
}
- String이 아래와 같이 생성되면 hello라는 문자열은 하나만 만들어진다.
- 모두 같은 문자열을 가리키고 있다. "hello"
- 하나를 바꿨을때, 모두 바뀌면 안되기때문에 String은 한번 생성되면 스스로 바뀌지 않는 "불변 클래스"이다.
<String클래스의 concat() 메소드>

이 메소드는 문자열 끝에 추가로 붙이는 기능을한다.
System.out.println(str3.concat("추가적인 concat"));
System.out.println(str3);
concat()으로 str3에 변형을 주어도 str3을 다시 출력해보면 그대로 “hello”가 출력되는 것을 확인할 수 있다.
<MyClass> - concat() 메소드 만들기
// 불변 클래스 String 예제를 위한 메소드
public int concat(int j){
i += j;
return i;
}
<StringExam02>
// i의 값이 실제로 바뀐 것을 확인할 수 있다.
System.out.println(myClass.concat(10));
System.out.println(myClass.i);
- 예제를 실행해보면 i의 값이 실제로 바뀌는데,
- 이는 불변 클래스 String이 concat()을 사용했을때 값이 바뀌지 않는 것 과는 다른 결과이다.
- String의 값을 바꿔주고 싶으면
str1 = str1.concat("String의 concat입니다.");
// 주소를 바꿔줘야한다.
<String이 불변클래스일 수 밖에 없는 이유>
concat()과 같은 메소드를 사용할때 추가되는 문자열로 인해 의도치 않은 값 변경이 일어나는 경우를 방지하기 위해
String을 참조하는 변수의 주소값을 직접 바꿔줘야한다.
str1 = str1.concat();
비슷한 예제를 살펴보면
public class ConcatStringExam {
private String name;
// 문자열 뒤에 값을 붙이는 메소드 concat을 만들어줌
public String concat(String word){
name = name + word;
return name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ConcatStringExam c1 = new ConcatStringExam();
c1.setName("김자바");
System.out.println(c1.getName());
// 1) 일반 클래스에서의 concat 예시
c1.concat("입니다.");
System.out.println(c1.getName());
System.out.println("--------------------------");
// 2) 불변 클래스에서의 concat 예시
String str1 = "김자바";
str1.concat("입니다.");
System.out.println(str1);
System.out.println("--------------------------");
// 3) concat으로 바뀐 값을 String에 적용해주고싶으면
// str1의 주소값을 새롭게 설정해주면 된다.
str1 = str1.concat("입니다.");
System.out.println(str1);
}
}
1번 예시의 경우
- name 필드 자체가 바뀌었기 때문에
- c1.getName()을 해주어도 "김자바+입니다"가 출력된다.
2번 예시의 경우
- 하지만 String은 그대로 "김자바"만 출력됨을 확인할 수 있다.
- "김자바입니다" 처럼 concat()이 적용된 문자열을 출력하고 싶으면
- str1 = str1.concat("입니다"); 로 새 주소값을 지정해주어야한다.
<불변클래스 String의 다양한 메소드>
- charAt()
: index를 입력받아, 그 인덱스에 해당하는 char타입 문자를 리턴한다.
String greeting = "Hello World!";
// 1) length() : 문자열의 길이를 리턴
System.out.println(greeting.length());
// 2) charAt(int index) (반환타입 char) : 특정 인덱스의 문자를 리턴
System.out.println(greeting.charAt(0)); // H 출력
- charAt()메소드 응용
// 2-1) 문자열 안에 'l'이 몇번 들어갔는지 확인 가능
int count = 0;
for(int i = 0; i < greeting.length(); i++){
if(greeting.charAt(i) == 'l'){
count++;
}
}
System.out.println("2-1) 일반 for문 : " + greeting + "안의 (l)의 개수 : " + count + "개");
// 2-2) 2-1을 메소드로 선언
findCount(greeting);
// 3) 문자열 안에 특정문자가 있는지 확인하는 boolean메소드
// boolean detector(String data, char findChar){}
// data안에 findChar가 존재하면 true, 없으면 false리턴
boolean result = detector(greeting, 'r');
System.out.println("3) [" + greeting + "]에서 [r]을 찾아본 결과 : " + result);
private static void findCount(String greeting) {
int count = 0;
for(int i = 0; i < greeting.length(); i++){
if(greeting.charAt(i) == 'l'){
count++;
}
}
System.out.println("2-2) findCount 메소드 : " + greeting + "안의 (l)의 개수 : " + count + "개");
}
private static boolean detector(String data, char findChar){
for(int i = 0; i < data.length(); i++){
if(data.charAt(i) == findChar) return true;
}
return false;
}
charAt() 메소드 응용에서 findCount(), detector()와 같은 메소드를 만들어서 활용해볼 수 있었다.
- toUpperCase() / toLowerCase()
: 문자열의 대소문자를 변환한다.
: String이 변하는 것이 아닌 새롭게 만들어진 문자열을 반환하는 것
// 대소문자 변환 (toUpperCase / toLowerCase)
String upper = greeting.toUpperCase();
String lower = greeting.toLowerCase();
System.out.println("대문자 변환: " + upper);
System.out.println("소문자 변환: " + lower);

- trim()
: 문자열의 공백을 제거한다.
// 공백 제거 (trim 메서드)
String spacedString = " Hello! ";
System.out.println("공백 제거 전 길이: " + spacedString.length());
String trimmed = spacedString.trim();
System.out.println("공백 제거 후 길이: " + trimmed.length());
System.out.println("trimmed: \\"" + trimmed + "\\"");

- substring()
: 문자열의 부분문자열을 추출한다.
// 부분 문자열 추출 (substring 메서드)
String substring = greeting.substring(7, 12); // "World" 추출
System.out.println("substring: " + substring);

<추상클래스 abstract>
추상클래스는 추상메소드를 포함할 수 있는 클래스
- 구체적인 인스턴스는 생성할 수 없으며, 상속을 통해서만 사용
- 공통적인 기능(필드, 메소드)를 정의하고, 일부 메소드는 “하위 클래스에서 반드시 구현하도록 “강제””
- 인스턴스화 불가 : new를 이용해 객체를 생성하지 못한다. 자식클래스로 객체를 생성해야한다.
- 상속을 통해 구현 확장 : 추상메소드를 구현하여 하위 클래스에서 “구체화”
- 구현메소드 포함 가능 : 추상클래스는 추상메소드 뿐만 아니라 일반 메소드도 포함할 수 있다.
- 필드, 생성자, 정적메소드 소유 가능 : 일반 클래스와 마찬가지로 필드, 생성자, static메소드를 포함할 수 있다.
<추상클래스 선언>
// 추상 클래스인 새 예제
public abstract class AbstractBird {
private String name;
private int age;
public void eat(){
System.out.println("새가 먹기 시작합니다.");
}
public void fly(){
System.out.println("새가 날기 시작합니다.");
}
/*
새가 어떻게 노래할까를 정의하려면,
새의 종류를 알아야하고 울음소리가 특정되지 않았으므로
추상적인 클래스라고 볼 수 있다.
"설계가 미완성"
*/
abstract public void song();
}
자식클래스들이 표준화된 방법으로 song()라는 메소드를 정의하게된다. (표준화)
자식클래스는 song()라는 메소드를 반드시 구현해야한다 (강제성)
<추상메소드 구조>
/*
새가 어떻게 노래할까를 정의하려면,
새의 종류를 알아야하고 울음소리가 특정되지 않았으므로
추상적인 클래스라고 볼 수 있다.
"설계가 미완성"
*/
public void song(){ };
추상메소드는 위와 같이 구현부가 있는 것이 아닌
public abstract void song();
처럼 구현부를 생략하고 앞에 abstract 키워드를 붙여 작성한다.
추상메소드는 하위 클래스에서 반드시 해당 메소드를 구현(Override)해야한다.
<추상클래스를 상속받은 자식클래스>
public class Crow extends AbstractBird{
@Override
public void song() {
System.out.println("추상클래스를 상속받은 [까마귀]가 노래합니다.");
}
}
// 추상클래스를 상속받은 자식클래스는 반드시 구현해야한다 (강제성)
public class Duck extends AbstractBird{
@Override
public void song() {
System.out.println("추상클래스를 상속받은 [오리]가 노래합니다.");
}
}
<추상클래스 실행 main메소드>
public static void main(String[] args) {
// 추상클래스이므로 인스턴스를 생성할 수 없다.
AbstractBird d = new Duck();
d.song();
AbstractBird c = new Crow();
c.song();
}
위에서 설명한
“Abstract class 는 추상메소드를 포함할 수 있는 클래스” 는
// 추상 클래스인 새 예제
public abstract class AbstractBird {
public abstract void song();
}
이러한 구조이다.
<Object의 equals(), toString()가 추상메소드가 아닌 이유>
먼저 추상메소드는 강제성을 가지고 있다.
그렇다면 Object를 상속받는 모든 클래스는 equals(), toString()를 반드시 구현해야하는 강제성을 가진다.
하지만 이 메소드들은 반드시 구현해야만 하는 기능은 아니었으므로, 강제성이 있는 추상메소드일 필요가 없는 것이다.
▶️<실습 - 도형 면적 구하기>
1. 추상클래스를 먼저 만든다.
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// 어떤 도형이든 면적을 계산하는 기능은 반드시 필요하므로
// 자식클래스에서 이 메소드 구현을 강제함
// 각 도형마다 면적을 구하는 방식은 다르므로, 자식클래스마다 다르게 구현되어야함
public abstract double getArea();
public String getColor() {
return color;
}
}
2. 이 추상클래스를 상속받고, 추상메소드 getArea()를 구현할 Circle클래스를 만든다.
public class Circle extends Shape{
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
3. Circle뿐만 아니라, Rectangle, Triangle 클래스도 만들어준다.
// Rectangle의 추상메소드 구현 예제
@Override
public double getArea() {
return width * height;
}
// Triangle의 추상메소드 구현 예제
@Override
public double getArea() {
return bottom * height;
}
4. 일반 출력
// 1) 인자로 값 지정해서 넣어주기
Shape c = new Circle("RED", 3);
System.out.println("원의 색깔 : " + c.getColor() + ", 원의 넓이 : " + String.format("%.1f", c.getArea()));
Shape r = new Rectangle("BLUE", 5, 10);
System.out.println("사각형의 색깔 : " + r.getColor() + ", 사각형의 넓이 : " + String.format("%.1f", r.getArea()));
Shape t = new Triangle("GREEN", 30, 3);
System.out.println("삼각형의 색깔 : " + t.getColor() + ", 삼각형의 넓이 : " + String.format("%.1f", t.getArea()));

5. Scanner로 입력받아 출력
// 2) Scanner로 사용자에게 입력받아 인자 넣어주기
Scanner sc = new Scanner(System.in);
for(int i = 0; i < 3; i++){
System.out.print("도형의 색깔을 지정 : ");
String color = sc.next();
switch (i){
case 0 : // 원을 의미
System.out.print("원의 반지름 : ");
double radius = sc.nextDouble();
Shape c2 = new Circle(color, radius);
System.out.println("원의 색깔 : " + c2.getColor() + ", 원의 넓이 : " + String.format("%.1f", c2.getArea()));
break;
case 1: // 사각형을 의미
System.out.print("사각형의 가로 : ");
double width = sc.nextDouble();
System.out.print("사각형의 세로 : ");
double height = sc.nextDouble();
Shape r2 = new Rectangle(color, width, height);
System.out.println("사각형의 색깔 : " + r2.getColor() + ", 사각형의 넓이 : " + String.format("%.1f", r2.getArea()));
break;
case 2: // 삼각형을 의미
System.out.print("삼각형의 높이 : ");
double tHeight = sc.nextDouble();
System.out.print("삼각형의 밑변 : ");
double tWidth = sc.nextDouble();
Shape t2 = new Rectangle(color, tHeight, tWidth);
System.out.println("삼각형의 색깔 : " + t2.getColor() + ", 삼각형의 넓이 : " + String.format("%.1f", t2.getArea()));
break;
}
System.out.println();
}

▶️<실습 - 추상클래스로 카페 시스템 만들기>
1.final키워드를 사용하여 final 메소드를 만든다. = 음료를 만들때 이 순서는 오버라이딩 되면 안된다는 의미이다.
// 이 메소드는 final로써 이 레시피의 순서를 오버라이딩(재정의)하지 못한다.
public final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
2. brew()와 addCondiments()는 추상메소드로 선언하고, Coffee나 Tea와 같은 자식클래스에서 오버라이딩으로 구현
public static void main(String[] args) {
Beverage c = new Coffee();
Beverage t = new Tea();
System.out.println("-----[커피] 주문 접수 : 만들기 시작-----");
c.prepareRecipe();
System.out.println("\n-----[차] 주문 접수 : 만들기 시작-----");
t.prepareRecipe();
}

<final 키워드>
- 필드에 final이 붙으면 상수 : 값을 바꿀 수 없는 숫자
- 메소드에 final이 붙으면 : 오버라이딩이 되지 않음을 의미
- 클래스에 final이 붙으면 : 더이상 상속을 금지한다는 것을 의미
<final 클래스>
- 이 클래스를 상속못한다는 것을 의미 (대를 끊는다는 것을 의미)
- Math클래스도 final로 지정되어있다.
만약 누군가가 Math가 가진 메소드를 임의대로 오버라이딩하면,
다른 사용자가 Math를 사용할때 오버라이딩된 자식의 메소드를 실행하기때문에 원치 않는 결과가 출력됨을 방지
<final 메소드>
- 오버라이딩 금지. 자식이 이 메소드를 수정해서 쓸 수가 없다.
- ⭐오버로딩은 가능하다. (메소드 이름만 같을뿐 매개변수가 다르기 때문에 다른 메소드로 취급된다)
💡 7일차에는 간단하지만 자칫 이해하기 어려울 수 있는 추상클래스에 대해 깊게 공부하였다. 처음 String클래스를 알고리즘으로 접하고 equals()와 toString()같은 메소드를 사용할때는 몰랐던 개념들과 문법을 더 자세히 알 수 있었다!
또한 final키워드를 사용하는 곳에 대해 상수만 알고있었는데 메소드와 클래스에서도 사용되면 어떠한 역할을 하는지 알 수 있게되었다. 관련 실습을 더 풀어보며 개념을 다져야겠다고 생각했다!🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_10일차_'내부클래스와 제네릭' (2) | 2024.12.13 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_8일차_'인터페이스와 예외처리' (0) | 2024.12.11 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_6일차_'상속과 메소드 오버라이딩' (1) | 2024.12.09 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_5일차_'메소드, 필드, static' (3) | 2024.12.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_4일차_'객체지향프로그래밍' (1) | 2024.12.05 |