본문 바로가기

항해 99/Java

Java 기초 7 - 클래스, 기본형과 참조형

클래스가 필요한 이유

자바는 클래스와 객체로 이루어져 있다(클래스와 객체라는 개념이 중요함).

 

문제 : 학생 정보 출력 프로그램

요구사항

  1. 첫 번째 학생의 이름은 "학생1", 나이는 15, 성적은 90
  2. 두 번째 학생의 이름은 "학생2", 나이는 16, 성적은 80
  3. 각 학생의 정보를 다음과 같은 형식으로 출력해야 함: "이름: [이름] 나이: [나이] 성적 : [성적]"
  4. 변수를 사용해서 학생 정보를 저장하고 변수를 사용해서 학생 정보를 출력
package class1;

public class ClassStart1 {
    public static void main(String[] args) {
        String student1Name = "학생1";
        int student1Age = 15;
        int student1Grade = 90;

        String student2Name = "학생 2";
        int student2Age = 16;
        int student2Grade = 80;

        System.out.println("이름:" + student1Name + "나이:" + student1Age + "성적:" + student1Grade);
        System.out.println("이름:" + student1Name + "나이:" + student1Age + "성적:" + student1Grade);
    }
}

 

위 코드의 문제점은 학생 수 증가에 따른 변수 추가 선언 및 출력 코드도 추가해야 함.

 

문제: 위 코드에 배열 사용

package class1;

public class ClassStart2 {
    public static void main(String[] args) {
        String[] studentsName = {"학생1", "학생2"};
        int[] studentsAge = {15, 16};
        int[] studentsGrade = {90, 80};

        for (int i = 0; i < studentsName.length; i++) {
            System.out.println("이름: " + studentsName[i] + " 나이: " + studentsAge[i] + " 성적: " + studentsGrade[i]);
        }
    }
}

 

배열 사용의 한계

배열을 사용해서 코드 변경을 최소화하는데 성공했지만, 한 학생의 데이터가 3개의 배열에 나누어져 있다. 따라서 데이터를 변경할 때 매우 조심해서 작업해야 한다(학생 2의 데이터를 제거하려면 각각의 배열마다 학생2의 요소를 정확하게 찾아서 제거해야 한다).

 

정리

지금처럼 이름, 나이, 성적을 각각 따로 나누어서 관리하는 것은 사람이 관리하기 좋은 방식이 아님(사람이 관리하기 좋은 방식은 학생이라는 개념을 하나로 묶어서 학생 안에 이름, 나이, 성적을 관리하는 것).

 

 

클래스 도입

Student 클래스

package class1;

public class Student {
    String name;
    int age;
    int grade;
}

 

class 키워드를 사용해서 학생 클래스(Student)를 정의한다. 학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가진다.

 

이렇게 클래스에 정의한 변수들을 멤버 변수, 또는 필드라 한다.

  • 멤버 변수(Member Variable): 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부른다
  • 필드(Field): 데이터 항목을 가르키는 전통적인 용어. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라 한다.
  • 자바에서 멤버 변수, 필드는 같은 뜻으로 클래스에 소속된 변수를 뜻한다.

클래스는 관례상 대문자로 시작하고 낙타 표기법을 사용한다.

예): Student, User, MemberService

 

ClassStart3

package class1;

public class ClassStart3 {
    public static void main(String[] args) {
        Student student1;
        student1 = new Student();
        student1.name = "학생1";
        student1.age = 15;
        student1.grade = 90;

        Student student2 = new Student();
        student2.name = "학생2";
        student2.age = 16;
        student2.grade = 80;

        System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
        System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
    }
}

 

클래스와 사용자 정의 타입

  • 타입은 데이터의 종류나 형태를 나타낸다.
  • int라고 하면 정수 타입, String이라고 하면 문자 타입이다.
  • 학생(Student)이라는 타입을 만들면 되지 않을까?
  • 클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있다.
  • 사용자가 직접 정의하는 사용자 정의 타입을 만들려면 설계도가 필요하다. 이 설계도가 바로 클래스이다.
  • 설계도인 클래스를 사용해서 실제 메모리에 만들어진 실체를 객체 또는 인스턴스라 한다.
  • 클래스를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할 수 있다.

용어: 클래스, 객체, 인스턴스

클래스는 설계도이고, 이 설계도를 기반으로 실제 메모리에 만들어진 실체를 객체 또는 인스턴스라 한다. 둘 다 같은 의미로 사용된다.

여기서는 학생(Student) 클래스를 기반으로 학생1(student1), 학생2(student2) 객체 또는 인스턴스를 만들었다.

 

 

1. 변수 선언

  • Student student1
    • Student 타입을 받을 수 있는 변수를 선언한다.
    • int는 정수를, String은 문자를 담을 수 있듯이 Student는 Student 타입의 객체(인스턴스)를 받을 수 있다.

 

2. 객체 생성

student1 = new Student( ) 코드를 나누어 분석

  • 객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 함
  • new Student( ): new는 새로 생성한다는 뜻, new Student( )는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻, 이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성함
  • 객체를 생성할 때는 new 클래스명( )을 사용하면 된다. 마지막에 ( )도 추가해야 함
  • Student 클래스는 String name, int age, int grade 멤버 변수를 가지고 있다. 이 변수를 사용하는데 필요한 메모리 공간도 함께 확보한다.

 

3. 참조값 보관

  • 객체를 생성하면 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)( x001 )을 반환한다.
    • 여기서 x001이라고 표현한 것이 참조값임
  • new 키워드를 통해 객체가 생성되고 나면 참조값을 반환한다. 앞서 선언한 변수인 Student student1에 생성된 객체의 참조값( x001)을 보관한다.
  • Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있음
    • student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있음, 따라서 이 변수를 통해서 객체를 접근(참조)할 수 있다(student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있음).

참조값을 변수에 보관해야 하는 이유

객체를 생성하는 new Student( ) 코드 자체에는 아무런 이름이 없음, 이 코드는 단순히 Student 클래스 기반으로 메모리에 실제 객체를 만드는 것.

생성한 객체에 접근할 수 있는 방법이 필요하기 때문에 객체를 생성할 때 반환되는 참조값을 보관해두어야 한다.

Student student1 변수에 참조값( x001)을 저장해두었으므로 저장한 참조값을 통해 실제 메모리에 존재하는 객체에 접근할 수 있음.

이후에 학생2(student2)까지 생성하면 다음과 같이 Student 객체(인스턴스)가 메모리에 2개 생성됨. 각각 참조값이 다르므로 서로 구분할 수 있음

참조값을 확인하고 싶은 경우 객체를 담고 있는 변수를 출력해보면 됨

System.out.println(student1);

 

 

객체 사용

클래스를 통해 생성한 객체를 사용하려면 먼저 메모리에 존재하는 객체에 접근해야 함(객체에 접근하려면 .(dot)을 사용하면 됨).

//객체 값 대입
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;

//객체 값 사용
 System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);

 

 

객체에 값 대입

객체가 가지고 있는 멤버 변수(name, age, grade)에 값을 대입하려면 먼저 객체에 접근해야 한다.

객체에 접근하려면 .(점, dot) 키워드를 사용하면 된다. 이 키워드는 변수(student1)에 들어있는 참조값(x001)을 읽어서 메모리에 존재하는 객체에 접근한다.

student1.name = "학생1" //1. student1 객체의 name 멤버 변수에 값 대입
x001.name "학생1" //2. 변수에 있는 참조값을 통해 실제 객체에 접근, 해당 객체의 name 멤버 변수에 값 대입

 

student1.name="학생1" 코드 실행 전

 

student1.name="학생1" 코드 실행 후

  • .(dot) 키워드 사용으로 해당 변수가 가지고 있는 참조값을 통해 실제 객체에 접근
  • x001(참조값) 객체가 있는 곳의 멤버 변수에 데이터가 저장됨.

 

객체 값 읽기

객체의 값을 읽는 것은 .(dot) 키워드를 통해 참조값을 사용해서 객체에 접근한 다음에 원하는 작업을 하면 된다.

// 1. 객체 값 읽기
System.out.println("이름:" + student1.name);
// 2. 변수에 있는 참조값을 통해 실제 객체에 접근하고, name 멤버 변수에 접근한다.
System.out.println("이름:" + x001.name);
// 3. 객체의 멤버 변수의 값을 읽어옴
System.out.println("이름:" + "학생1");

 

  • x001에 있는 Student 인스턴스의 name 멤버 변수는 "학생1"이라는 값을 가지고 있다.

 

클래스, 객체, 인스턴스 정리

클래스 - Class

클래스는 객체를 생성하기 위한 '틀' 또는 '설계도'. 클래스는 객체가 가져야 할 속성(변수)과 기능(메서드)를 정의.(학생 클래스는 속성으로 name, age, grade를 가짐)

  • 틀: 예) 붕어빵 틀은 실제 먹을 수 있는 것이 아님, 실제 먹을 수 있는 붕어빵이 객체 또는 인스턴스임.
  • 설계도: 설계도는 실제 존재하는 것이 아니라 개념으로만 있는 것, 설계도를 통해 만들어진 실제 존재하는 것이 객체 또는 인스턴스.

객체 - Object

객체는 클래스에서 정의한 속성과 기능을 가진 실체, 객체는 서로 독립적인 상태를 가진다. (student1과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체)

 

인스턴스 - Instance

인스턴스는 특정 클래스로부터 생성된 객체를 의미(객체와 인스턴스라는 용어는 자주 혼용됨). 인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용. (student1 객체는 클래스의 인스턴스)

 

객체 vs 인스턴스

둘 다 클래스에서 나온 실체라는 의미에서는 비슷하게 사용되나, 용어상 인스턴스는 객체보다 좀 더 관계에 초점을 맞춘 단어(특정 클래스와의 관계를 명확히 할 때 인스턴스라는 용어를 주로 사용).

모든 인스턴스는 객체이지만, 인스턴스라고 부르는 순간은 특정 클래스로부터 그 객체가 생성되었음을 강조하고 싶을 때임.(studnet1은 객체, student1은 Student의 인스턴스)

 

둘 다 클래스에서 나온 실체라는 핵심 의미가 같아 보통 둘을 구분하지 않고 사용함.

 

 

배열 도입

배열을 사용하면 특정 타입을 연속한 데이터 구조로 묶어서 편리하게 관리할 수 있다.

Student 클래스를 사용한 변수들도 Student 타입이기 때문에 학생도 배열을 사용해서 하나의 데이터 구조로 묶어서 관리할 수 있다.

 

Student 타입을 사용하는 배열 도입

ClassStart4

package class1;

public class ClassStart4 {
    public static void main(String[] args) {
        Student student1 = new Student();
        student1.name = "학생1";
        student1.age = 15;
        student1.grade = 90;

        Student student2 = new Student();
        student2.name = "학생2";
        student2.age = 16;
        student2.grade = 80;

        Student[] students = new Student[2];
        students[0] = student1;
        students[1] = student2;

        System.out.println("이름:" + students[0].name + " 나이:" + students[0].age + " 성적:" + students[0].grade);
        System.out.println("이름:" + students[1].name + " 나이:" + students[1].age + " 성적:" + students[1].grade);
    }
}

 

Student 클래스를 기반으로 student1,  student2 인스턴스를 생성, 필요한 값을 채워둔다.

 

 

배열에 참조값 대입

Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관

Student[] students = new Student[2];

 

  • Student 변수를 2개 보관할 수 있는 사이즈 2의 배열을 만든다.
  • Student 타입의 변수는 Student 인스턴스의 참조값을 보관, Student 배열의 각각의 항목도 Student 타입의 변수일 뿐(Student 타입의 참조값을 보관함).
    • studnet1, student2 변수를 생각해보면 Student 타입의 참조값을 보관한다.
  • 배열에는 아직 참조값을 대입하지 않았기 대문에 참조값이 없다는 의미의 null값으로 초기화 됨.
students[0] = student1;
students[1] = student2;

// java의 대입은 항상 변수에 있는 값을 복사
students[0] = x001;
students[1] = x002;

 

 

  • student1, student2에 보관된 참조값은 배열에 저장됨.
  • 배열은 참조값을 가기고 있어 x001(학생1), x002(학생2) Student 인스턴스에 모두 접근할 수 있음
  • 자바에서 대입은 항상 변수에 들어 있는 값을 복사해서 전달하기 때문에 기존 student1, student2에 들어있던 참조값은 그대로 유지됨.

주의!

변수에는 인스턴스 자체가 들어있는 것이 아님, 인스턴스의 위치를 가리키는 참조값이 들어있음(대입(=) 시 참조값만 복사된다(인스턴스 복사 x))

 

 

배열에 들어있는 객체 사용

배열에 들어있는 객체를 사용하려면 배열에 접근 후 객체에 접근하면 된다.

System.out.println(studnets[0].name); // 배열 접근 시작
System.out.println(x005.name); // [0]을 사용해서 x005 배열의 0번 요소에 접근
System.out.println(x001.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생1");

 

 

배열 도입 - 리펙토링

ClassStart5

package class1;

public class ClassStart5 {
    public static void main(String[] args) {
        Student student1 = new Student();
        student1.name = "학생1";
        student1.age = 15;
        student1.grade = 90;

        Student student2 = new Student();
        student2.name = "학생2";
        student2.age = 16;
        student2.grade = 80;

        Student[] students = {student1, student2};

        // 일반 for문
//        for (int i = 0; i < students.length; i++) {
//            System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade);
//        }
        
        // for문 - 자주 사용하는 객체 변수에 담아 사용
//        for (int i = 0; i < students.length; i++) {
//            Student s = students[i];
//            System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade);
//        }
        
        //향상된 for문 사용
        for (Student s : students) {
            System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade);
        }
    }
}

 

배열 선언 최적화

Student 타입도 일반적인 변수와 동일하게 배열을 생성할 때 포함할 수 있음

Student[] students = {student1, student2};
  • 생성과 선언을 동시에 하는 것으로 최적화 할 수 있음

for문 최적화

배열을 사용한 덕에 for문을 사용해서 반복 작업을 깔끔하게 처리할 수 있음

for (int i = 0; i < students.length; i++) {
            System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade);
        }

 

for문 - 반복 요소를 변수에 담아서 처리

for (int i = 0; i < students.length; i++) {
    Student s = students[i];
    System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade);
}
  • 반복해서 사용하는 객체를 Student s와 같은 변수에 담아두고 사용해도 됨.

향상된 for문(Enhanced For Loop)

for (Student s : students) {
    System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" + s.grade);
}

 

 

문제 풀이

1. 영화 리뷰 관리하기 1

영화 리뷰 정보를 담을 수 있는 MovieReview 클래스를 만들기

요구사항 

  1. MovieReview 클래스는 다음과 같은 멤버 변수를 포함해야 한다
    • 영화 제목(title)
    • 리뷰 내용(review)
  2. MovieReviewMain 클래스 안에 main( ) 메서드를 포함하여, 영화 리뷰 정보를 선언하고 출력

출력 예시

영화 제목: "인셉션", 리뷰: "인생은 무한 루프"
영화 제목: "어바웃 타임", 리뷰: "인생 시간 영화!"

 

MovieReview

package class1.ex;

public class MovieReview {
    String title;
    String review;
}

 

MovieReviewMain

package class1.ex;

public class MovieReviewMain {
    public static void main(String[] args) {
        //영화 리뷰 정보 선언
        MovieReview movie1 = new MovieReview();
        movie1.title = "인셉션";
        movie1.review = "인생은 무한 루프";
        MovieReview movie2 = new MovieReview();
        movie2.title = "어바웃 타임";
        movie2.review = "인생 시간 영화!";

        MovieReview movies[] = {movie1, movie2};

        // 영화 리뷰 정보 출력
        for (MovieReview m : movies) {
            System.out.println("영화 제목: " + m.title + ", 리뷰: " + m.review);
        }
    }
}

 

 

2. 영화 리뷰 관리하기2

기존 문제에 배열을 도입하고, 영화 리뷰를 배열에 관리하자.

리뷰 출력 시 배열과 for문을 사용해서 System.out.println을 한 번만 사용하자.

 

1번 문제에서 배열 사용해서 관리 및 출력 완료

 

 

3. 상품 주문 시스템 개발

온라인 상점의 주문 관리 시스템 만든다.

상품 주문 정보를 담을 수 있는 ProductOrder 클래스를 제작

요구 사항

  1. ProductOrder 클래스는 다음과 같은 멤버 변수를 포함해야 한다
    • 상품명(productName)
    • 가격(price)
    • 주문 수량(quantity)
  2. ProductOrderMain 클래스 안에 main( ) 메서드를 포함하여, 여러 상품의 주문 정보를 배열로 관리하고, 그 정보들을 출력하고, 최종 금액을 계산하여 출력.
  3. 출력 예시와 같도록 출력

예시코드

public class ProductOrderMain {
    public static void main(String[] args) {
        // 여러 상품의 주문 정보를 담는 배열 생성
        // 상품 주문 정보를 'ProductOrder' 타입의 변수로 받아 저장
        // 상품 주문 정보와 최종 금액 출력
    }
}

 

출력 예시

상품명: 두부, 가격: 2000, 수량: 2
상품명: 김치, 가격: 5000, 수량: 1
상품명: 콜라, 가격: 1500, 수량: 2
총 결제 금액: 12000

 

ProductOrder

package class1.ex;

public class ProductOrder {
    String productName;
    int price;
    int quantity;
}

 

ProductOrderMain

package class1.ex;

public class ProductOrderMain {
    public static void main(String[] args) {
        // 총 결제 금액 담을 변수
        int totalCost = 0;
        
        // 상품 주문 정보를 ProductOrder 타입의 변수로 받아 저장
        ProductOrder order1 = new ProductOrder();
        order1.productName = "두부";
        order1.price = 2000;
        order1.quantity = 2;
        ProductOrder order2 = new ProductOrder();
        order2.productName = "김치";
        order2.price = 5000;
        order2.quantity = 1;
        ProductOrder order3 = new ProductOrder();
        order3.productName = "콜라";
        order3.price = 1500;
        order3.quantity = 2;

        // 여러 상품의 주문 정보를 담는 배열 생성
        ProductOrder orders[] = {order1, order2, order3};

        // 상품 주문 정보와 최종 금액 출력
        for (ProductOrder order : orders) {
            totalCost += order.price * order.quantity;
            System.out.println("상품명: " + order.productName + ", 가격: " + order.price + ", 수량: " + order.quantity);
        }
        System.out.println("총 결제 금액: " + totalCost);
    }
}

 

 

정리

 

 

기본형과 참조형

자바에서 참조형을 제대로 이해하는 것은 정말 중요함

변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있다. 사용하는 값을 변수에 직접 넣을 수 있는 기본형, 객체가 저장된 메모리의 위치를 가리키는 참조값을 넣을 수 있는 참조형으로 분류

 

기본형 vs 참조형

  • 기본형(Primitive Type): int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입
  • 참조형(Reference Type): Student student1, int[ ] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 참조형이라 함. 참조형은 객체 또는 배열에 사용 됨.

참조형 변수를 통해서 뭔가 하려면 참조값을 통해 해당 위치로 이동해야 한다.

 

기본형 vs 참조형 - 기본

  • 기본형은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있다. 해당 값을 바로 사용할 수 있음
  • 참조형은 실제 사용하는 값을 변수에 담는 것이 아님, 실제 객체의 위치(참조, 주소)를 저장한다. 참조형에는 객체와 배열이 있음
    • 객체는 .(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있음
    • 배열은 [ ] 통해서 메모리 상에 생성된 배열을 찾아가야 사용할 수있음

기본형 vs 참조형 - 계산

  • 기본형은 들어있는 값을 그대로 계산에 사용할 수 있다.
    • 더하고 빼고, 사용하고 등등, (숫자 같은 것들은 바로 계산할 수 있음)
  • 참조형은 들어있는 참조값을 그대로 사용할 수 없다. 주소지만 가지고는 할 수 있는 게 없음, 주소지에 가야 실체가 있음.
    • 예) 더하고 뺴고 사용하고 못함, 참조값만 가지고는 계산할 수 있는 것이 없다.
// 기본형 변수는 연산 가능
int a = 10, b = 20;
int sum = a + b;

//참조형 변수는 .을 통해 객체의 기본형 멤버 변수에 접근해야 연산 가능
Student s1 = new Student();
Student s2 = new Student();
s1 + s2; // 오류 발생

s1.grade = 100;
s2.grade = 90;
int sum = s1.grade + s2.grade; // 연산 가능

 

쉽게 이해하는 팁

기본형을 제외한 나머지는 모두 참조형이다.

  • 기본형은 소문자로 시작한다. int, long, double, boolean 모두 소문자로 시작
    • 기본형은 자바가 기본으로 제공하는 데이터 타입, 기본형은 개발자가 새로 정의할 수 없다.(개발자는 참조형인 클래스만 직접 정의할 수 있음)
  • 클래스는 대문자로 시작한다. Student
    • 클래스는 모두 참조형이다.

참고 - String

자바에서 String은 특별하다, String은 사실은 클래스이며 참조형이다. 그런데 기본형처럼 문자 값을 바로 대입할 수 있다. 문자는 매우 자주 다루기 때문에 자바에서 특별한 편의 기능을 제공한다.

 

 

기본형 vs 참조형2 - 변수 대입

대원칙: 자바는 항상 변수의 값을 복사해서 대입한다.

기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다(기본형은 변수에 들어있는 실제 사용하는 값을 복사해서 대입, 참조형은 변수에 들어 있는 참조값을 복사해서 대입)

 

이 대원칙을 이해하면 복잡한 상황에도 코드를 단순하게 이해할 수 있음.

//기본형 대입
int a = 10;
int b = a;
//참조형 대입
Student s1 = new Student();
Student s2 = s1;

 

  • 참조형은 실제 사용하는 객체가 아니라 객체의 위치를 가르키는 참조값만 복사됨(실제 건물이 복사되는 것이 아니라 건물의 위치인 주소만 복사됨 - 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐임).

 

기본형과 변수 대입

VarChange1

package ref;

public class VarChange1 {
    public static void main(String[] args) {
        int a = 10;
        int b = a;
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        // a 변경
        a = 20;
        System.out.println("변경 a = 20");
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        // b 변경
        b = 30;
        System.out.println("변경 b = 30");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

 

핵심 : int b = a라고 했을 때 변수에 들어있는 값을 복사해서 전달한다는 점, 따라서 a=20, b=30이라고 했을 때 각각 본인의 값만 변경되는 것을 확인할 수 있음.

 

 

참조형과 변수 대입

참조형 예시를 위한 Data 클래스 제작, 이 클래스는 단순히 int value라는 멤버 변수를 하나 가진다.

Data

package ref;

public class Data {
    int value;
}

 

VarChange2

package ref;

public class VarChange2 {
    public static void main(String[] args) {
        Data dataA = new Data();
        dataA.value = 10;
        Data dataB = dataA;

        System.out.println("dataA 참조값=" + dataA);
        System.out.println("dataB 참조값=" + dataB);
        System.out.println("dataA.value =" + dataA.value);
        System.out.println("dataB.value =" + dataB.value);

        //dataA 변경
        dataA.value = 20;
        System.out.println("변경 dataA.value = 20");
        System.out.println("dataA.value =" + dataA.value);
        System.out.println("dataB.value =" + dataB.value);

        //dataB 변경
        dataB.value = 30;
        System.out.println("변경 dataB.value = 30");
        System.out.println("dataA.value =" + dataA.value);
        System.out.println("dataB.value =" + dataB.value);
    }
}

 

그림 설명

 

dataA 변수는 Data 클래스를 통해서 만들었기 때문에 참조형, 이 변수는 Data형 객체의 참조값을 저장.

Data 객체를 생성하고, 참조값을 dataA에 저장한다. 그리고 객체의 value 변수에 값 10을 저장함.

 

 

변수의 대입은 변수에 들어있는 값을 복사해서 대입한다. 변수 dataA에는 참조값 x001이 들어있다. dataB에는 변수 dataA에 들어있는 참조값 x001이 복사되어 대입된다(참고로 dataA가 가리키는 인스턴스를 복사하는 것이 아님, 변수에 들어있는 참조값만 복사해서 전달한다).

dataA와 dataB에 들어있는 참조값은 같다. 둘 다 같은 x001 Data 인스턴스를 가리킨다.

 

 

dataA.value = 20 코드를 실행하면 dataA가 가리키는 x001 인스턴스의 value값을 10에서 20으로 변경한다. dataB는 dataA와 같은 x001인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 둘 다 같은 값인 20을 출력한다.

 

 

dataB.value = 30 코드를 실행하면 dataB가 가리키는 x001 인스턴스의 value 값을 20에서 30으로 변경한다. dataA는 dataB와 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 둘 다 같은 값인 30을 출력한다.

 

핵심 : Data dataB = dataA라고 했을 때 변수에 들어있는 값을 복사해서 사용한다는 점(그 값이 참조값). dataA와 dataB는 같은 참조값을 가지게 되고, 두 변수는 같은 객체 인스턴스를 참조하게 된다.

 

 

기본형 vs 참조형 3 - 메서드 호출

대원칙: 자바는 항상 변수의 값을 복사해서 대입한다.

메서드 호출도 마찬가지임, 메서드 호출에 사용하는 매개변수(파라미터)도 결국 변수일 뿐. 따라서 메서드를 호출할 때 매개변수에 값을 전달하는 것도 값을 복사해서 전달한다.

 

기본형과 메서드 호출

package ref;

public class MethodChange1 {
    public static void main(String[] args) {
        int a = 10;
        System.out.println("메서드 호출 전: a = " + a);
        changePrimitive(a);
        System.out.println("메서드 호출 후: a = " + a);
    }

    public static void changePrimitive(int x) {
        x = 20;
    }
}

 

1. 메서드 호출

 

메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달한다(변수 a의 값을 복사해서 x에 전달하므로 a, x는 각각 숫자 10을 가짐)

 

2. 메서드 안에서 값을 변경

 

메서드 안에서 x = 20으로 새로운 값을 대입한다(a의 값은 변경되지 않고 10으로 유지 된다).

 

3. 메서드 종료

 

메서드 종료 후 값을 출력 시 a는 10이 출력된다(매개 변수 x는 메서드 종료와 함께 제거된다).

 

 

참조형과 메서드 호출

package ref;

public class MethodChange2 {
    public static void main(String[] args) {
        Data dataA = new Data();
        dataA.value = 10;
        System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
        changePrimitive(dataA);
        System.out.println("메서드 호출 후: dataA.value = " + dataA.value);
    }

    public static void changePrimitive(Data dataX) {
        dataX.value = 20;
    }
}

 

Data 인스턴스를 생성하고, 참조값을 dataA 변수에 담고 value에 숫자 10을 할당한 상태는 다음과 같음

 

1. 메서드 호출

 

메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 복사해서 전달(dataA와 dataX 둘 다 같은 참조값인 x001을 가짐). 따라서 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있다.

 

2. 메서드 안에서 값을 변경

 

메서드 안에서 dataX.value = 20으로 새로운 값을 대입.

참조값을 통해서 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경, dataA와 dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value, dataX.value 둘 다 20이라는 값을 가짐.

 

3. 메서드 종료

 

메서드 종료 후 dataA.value의 값을 확인해보면 값이 20으로 변경된 것을 확인할 수 있음.

 

기본형과 참조형의 메서드 호출

자바에서 메서드의 매개변수(파라미터)는 항상 값에 의해 전달됨. 그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라짐

  • 기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달됨. 이 경우, 메서드 내부에서 매개변수(파라미터)의 값을 변경해도, 호출자의 변수 값에는 영향이 없음
  • 참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달됨. 이 경우, 메서드 내부에서 매개변수(파라미터)의 값을 변경 시 호출자의 객체도 변경 됨.

 

 

참조형과 메서드 호출 - 활용

이전 코드 class1.ClassStart3 코드에는 중복되는 부분 2가지 있음

  • name, age, grade에 값을 할당
  • 학생 정보를 출력

ClassStart3

package class1;

public class ClassStart3 {
    public static void main(String[] args) {
        Student student1;
        student1 = new Student();
        student1.name = "학생1";
        student1.age = 15;
        student1.grade = 90;

        Student student2 = new Student();
        student2.name = "학생2";
        student2.age = 16;
        student2.grade = 80;

        System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
        System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
    }
}

 

이런 중복은 메서드를 통해 쉽게 제거가 가능함.

 

 

메서드에 객체 전달

package ref;

public class Method1 {
    public static void main(String[] args) {
        Student student1 = new Student();
        initStudent(student1, "학생1", 15, 90);

        Student student2 = new Student();
        initStudent(student2, "학생2", 16, 80);

        printStudent(student1);
        printStudent(student2);
    }

    static void initStudent(Student student, String name, int age, int grade) {
        student.name = name;
        student.age = age;
        student.grade = grade;
    }

    static void printStudent(Student student) {
        System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
    }
}

 

참조형은 메서드를 호출할 때 참조값을 전달한다. 따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있음.

  • initStudent(Student student, ...) : 전달한 학생 객체의 필드에 값을 설정한다.
  • printStudent(Student student, ...): 전달한 학생 객체의 필드 값을 읽어서 출력함

initStudent( )메서드 호출 분석

 

  • student1이 참조하는 Student 인스턴스에 값을 편리하게 할당하기 위해 initStudent( ) 메서드를 제작
  • 이 메서드를 호출하면서 student1을 전달, 그러면 student1의 참조값이 매개변수 student에 전달됨. 참조값을 통해서 initStudent( ) 메서드 안에서 student1이 참조하는 것과 동일한 x001 Student 인스턴스에 접근하고 값을 변경할 수 있음

주의!

  • import class1.Student;이 선언되어 있으면 안 됨.
  • 이 경우 class1 패키지에서 선언한 Student를 사용하게 되며, 접근 제어자 때문에 컴파일 오류가 발생함.
  • 선언되어 있을 경우 삭제 후 같은 패키지에 있는 ref.Student를 사용

 

메서드 객체 반환

package ref;

public class Method2 {
    public static void main(String[] args) {
        Student student1 = createStudent("학생1", 15, 90);
        Student student2 = createStudent("학생2", 16, 80);

        printStudent(student1);
        printStudent(student2);
    }

    static Student createStudent(String name, int age, int grade) {
        Student student = new Student();
        student.name = name;
        student.age = age;
        student.grade = grade;
        return student;
    }

    static void printStudent(Student student) {
        System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
    }
}

 

createStudent( ) 메서드를 만들고 객체 생성도 메서드 안에 포함시켜 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리함.

메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있도록 메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환한다(메서드는 호출 결과를 반환할 수 있음).

 

createStudent( ) 메서드 호출 분석

 

메서드 내부에서 인스턴스를 생성한 후 참조값을 메서드 외부로 반환(이 참조값만 있으면 해당 인스턴스에 접근할 수 있다, 여기서는 student1에 참조값을 보관해서 사용).

 

 

변수와 초기화

변수의 종류

  • 멤버 변수(필드) : 클래스에 선언
  • 지역 변수 : 메서드에 선언, 매개변수도 지역 변수의 한 종류

멤버 변수, 필드 예시

public class Student {
    String name;
    int age;
    int grade;
}

name, age, grade는 멤버 변수

 

지역 변수 예시

public class ClassStart3 {
    public static void main(String[] args) {
        Studnet student1;
        student1 = new Student();
        Student Student2 = new Student();
    }
}

student1, student2는 지역 변수

package ref;

public class MethodChange1 {
    public static void main(String[] args) {
        int a = 10;
        System.out.println("메서드 호출 전: a = " + a);
        changePrimitive(a);
        System.out.println("메서드 호출 후: a = " + a);
    }

    public static void changePrimitive(int x) {
        x = 20;
    }
}

a, x(매개변수)는 지역 변수

  • 지역변수는 특정 지역에서만 사용되는 변수(변수 x는 changePrimitive( ) 메서드 블록에서만 사용되며 메서드 종료 시 제거된다).

 

변수의 값 초기화

  • 멤버 변수: 자동 초기화
    • 인스턴스 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화됨.
    • 숫자(int) = 0, boolean = false, 참조형 = null (참조 대상이 없다는 뜻)
    • 개발자가 초기값을 직접 지정할 수 있음
  • 지역 변수 : 수동 초기화
    • 지역 변수는 항상 직접 초기화해야 함.

멤버 변수의 초기화

InitData

package ref;

public class InitData {
    int value1; // 초기화 하지 않음
    int value2 = 10; // 10으로 초기화
}

 

InitMain

package ref;

public class InitMain {
    public static void main(String[] args) {
        InitData data = new InitData();
        System.out.println("value1 = " + data.value1);
        System.out.println("value2 = " + data.value2);
    }
}

 

value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 되고, 숫자는 0으로 초기화 된다.

value2는 10으로 초기값을 지정해서 객체를 생성할 때 10으로 초기화 된다.

 

 

null

참조형 변수에서 아직 가리키는 대상이 없는 경우 null이라는 특별한 값을 넣어둘 수 있음, null은 값이 존재하지 않는, 없다는 뜻.

 

null값 할당

package ref;

public class NullMain1 {
    public static void main(String[] args) {
        Data data = null;
        System.out.println("1. data = " + data);
        data = new Data();
        System.out.println("2. data = " + data);
        data = null;
        System.out.println("3. data = " + data);
    }
}

 

 

Data 타입을 받을 수 있는 참조형 변수 data를 만들고 null 값을 할당하면 data 변수에는 가리키는 객체가 없다.

 

이후에 새로운 Data 객체를 생성해서 그 참조값을 data 변수에 할당하면 data 변수에 참조할 객체가 존재함.

 

data = null

 

마지막으로 data에 다시 null 값을 할당하면 data 변수는 앞서 만든 Data 인스턴스는 더 이상 참조하지 않는다.

 

GC - 아무도 참조하지 않는 인스턴스의 최후

 

data에 null을 할당하면 앞서 생성한 x001 Data 인스턴스를 더 이상 아무도 참조하지 않게 됨. 아무도 참조하지 않게 되면 x001이라는 참조값을 다시 구할 방법은 없게 되고, 따라서 해당 인스턴스에 다시 접근할 방법이 없다.

아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지하기 때문에 JVM의 GC(가비지 컬렉터)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 메모리에서 제거한다.

객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료할 때까지 계속 생존한다. 중간에 해당 객체를 참조하는 곳이 모두 사라지면 JVM은 필요 없는 객체로 판단하고 GC를 사용해서 제거함.

 

 

NullPointerException

참조값 없이 객체를 찾아가면 NullPointerException이라는 예외가 발생한다(주소가 없는 곳을 찾아갈 때 발생하는 예외).

 

객체를 참조할 때는 .(dot)을 사용한다. 이렇게 하면 참조값을 사용해서 해당 객체를 찾아갈 수 있다. 참조값이 null이라면 값이 없기 때문에 찾아갈 수 있는 객체(인스턴스)가 없다. NullPointerException은 null에 .(dot)을 찍었을 때 발생한다.

 

예제

package ref;

public class NullMain2 {
    public static void main(String[] args) {
        Data data = null;
        data.value = 10; // NullPointerException 예외 발생
        System.out.println("data = " + data.value);
    }
}

 

null은 참조할 주소가 존재하지 않기 때문에 참조할 객체 인스턴스가 없다(NullPointerException 예외 발생). 예외가 발생하면 프로그램이 종료되고 그 다음 로직은 수행되지 않는다.

 

 

멤버 변수 null

멤버 변수가 null인 경우에는 주의가 필요함(지역 변수가 null인 경우에는 문제를 파악하는 것이 어렵지 않다).

bigData 클래스

package ref;

public class BigData {
    Data data;
    int count;
}

 

NullMain3

package ref;

public class NullMain3 {
    public static void main(String[] args) {
        BigData bigData = new BigData();
        System.out.println("bigData.count=" + bigData.count);
        System.out.println("bigData.data=" + bigData.data);

        //NullPointerException;
        System.out.println("bigData.data.value" + bigData.data.value);
    }
}

 

 

BigData를 생성하면 BigData의 인스턴스가 생성 됨. 이 때 BigData 인스턴스의 멤버 변수에 초기화가 일어나는데, BigData의 data 멤버 변수는 참조형이므로 null로 초기화 된다. count 멤버 변수는 숫자이므로 0으로 초기화 된다.

  • bigData.count를 출력하면 0이 출력됨
  • bigData.data를 출력하면 참조값인 null이 출력 됨(해당 변수는 아무것도 참조하고 있지 않음).
  • bigData.data.value를 출력하면 data의 값이 null이므로 null에 .(dot)을 찍게 되고, 참조할 곳이 없으므로 NullPointerException 예외가 발생

 

package ref;

public class NullMain4 {
    public static void main(String[] args) {
        BigData bigData = new BigData();
        bigData.data = new Data();
        System.out.println("bigData.count=" + bigData.count);
        System.out.println("bigData.data=" + bigData.data);

        //NullPointerException;
        System.out.println("bigData.data.value" + bigData.data.value);
    }
}

 

 

정리

NullPointerException이 발생하면 null 값에 .(dot)을 찍었다고 생각하면 문제를 쉽게 찾을 수 있음

 

 

문제와 풀이

문제 : 상품 주문 시스템 개발 - 리팩토링

class1.ex.ProductOrderMain을 리팩토링

 

요구사항

ProductOrder 클래스는 다음과 같은 멤버 변수를 포함해야 함

  • 상품명 (productName)
  • 가격 (price)
  • 주문 수량 (quantity)

예시 코드

package ref.ex;

public class ProductOrder {
    String productName;
    int price;
    int quantity;
}

 

ref.ex.ProductOrderMain2 클래스 안에 main( ) 메서드를 포함하여, 여러 상품의 주문 정보를 배열로 관리하고, 그 정보들을 출력하고, 최종 결제 금액을 계산하여 출력.

해당 클래스에서는 다음 메서드를 포함해야 한다.

  • static ProductOrder createOrder(String productName, int price, int quantity)
    • ProductOrder를 생성하고 매개변수로 초기값을 설정, 마지막으로 생성한 ProductOrder를 반환
  • static void printOrders(ProductOrder[ ] orders)
    • 배열을 받아서 배열에 들어있는 전체 ProductOrder의 상품명, 가격, 수량을 출력
  • static int getTotalAmount(ProductOrder[ ] orders)
    • 배열을 받아서 배열에 들어있는 전체 ProductOrder의 총 결제 금액을 계산하고, 계산 결과를 반환
package ref.ex;

public class ProductOrderMain2 {
    public static void main(String[] args) {
        // 여러 상품의 주문 정보를 담는 배열
        ProductOrder productOrders[] = new ProductOrder[3];

        // createOrder 사용으로 주문 정보 생성 및 배열에 저장
        productOrders[0] = createOrder("두부", 2000, 2);
        productOrders[1] = createOrder("김치", 5000, 1);
        productOrders[2] = createOrder("콜라", 1500, 2);

        // printOrders로 상품 주문 정보 출력
        printOrders(productOrders);

        // getTotalAmount로 상품 주문 정보 출력
        getTotalAmount(productOrders);
    }

    static ProductOrder createOrder(String productName, int price, int quantity) {
        ProductOrder order = new ProductOrder();
        order.productName = productName;
        order.price = price;
        order.quantity = quantity;
        return order;
    }

    static void printOrders(ProductOrder[] orders) {
        for (ProductOrder order : orders) {
            System.out.println("상품명: " + order.productName + ", 가격: " + order.price + ", 수량: " + order.quantity);
        }
    }

    static int getTotalAmount(ProductOrder[] orders) {
        int totalAmount = 0;
        for (ProductOrder order : orders) {
            totalAmount += order.price * order.quantity;
        }
        System.out.println("총 결제 금액: " + totalAmount);
        return totalAmount;
    }
}

 

 

2. 상품 주문 시스템 개발 - 사용자 입력

상품 주문 시스템을 사용자 입력을 받도록 개선

 

요구 사항

  • 아래 입력, 출력 예시를 참고해서 다음 사항을 적용
  • 주문 수량을 입력 받기
    • 예) 입력할 주문의 개수를 입력하세요:
  • 가격, 수량, 상품명을 입력 받기
    • 입력시 상품 순서를 알 수 있게 "n번째 주문 정보를 입력하세요."라는 메시지를 출력
  • 입력이 끝나면 등록한 상품과 총 결제 금액을 출력

예시

 

package ref.ex;

import java.util.Scanner;

public class ProductOrderMain3 {
    public static void main(String[] args) {
        // 사용자 입력
        Scanner scanner = new Scanner(System.in);

        // 사용자 입력을 통한 배열 생성
        System.out.print("입력할 주문의 개수를 입력하세요: ");
        int orderQuantity = scanner.nextInt();
        scanner.nextLine();
        ProductOrder productOrders[] = new ProductOrder[orderQuantity];

        // createOrder 사용으로 주문 정보 생성 및 배열에 저장
        for (int i = 0; i < orderQuantity; i++) {
            System.out.println((i+1)+"번째 주문 정보를 입력하세요.");

            System.out.print("상품명: ");
            String productName = scanner.nextLine();

            System.out.print("가격: ");
            int price = scanner.nextInt();

            System.out.print("수량: ");
            int quantity = scanner.nextInt();
            scanner.nextLine();
            productOrders[i] = createOrder(productName, price, quantity);
        }

        // printOrders로 상품 주문 정보 출력
        printOrders(productOrders);

        // getTotalAmount로 상품 주문 정보 출력
        getTotalAmount(productOrders);
    }

    static ProductOrder createOrder(String productName, int price, int quantity) {
        ProductOrder order = new ProductOrder();
        order.productName = productName;
        order.price = price;
        order.quantity = quantity;
        return order;
    }

    static void printOrders(ProductOrder[] orders) {
        for (ProductOrder order : orders) {
            System.out.println("상품명: " + order.productName + ", 가격: " + order.price + ", 수량: " + order.quantity);
        }
    }

    static int getTotalAmount(ProductOrder[] orders) {
        int totalAmount = 0;
        for (ProductOrder order : orders) {
            totalAmount += order.price * order.quantity;
        }
        System.out.println("총 결제 금액: " + totalAmount);
        return totalAmount;
    }
}

 

 

정리

대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다.

자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것.

기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입함, 기본형은 변수에 들어 있는 실제 사용하는 값을 복사해서 대입, 참조형은 변수에 들어 있는 참조값을 복사해서 대입

기본형이든 참조형이든 변수의 값을 대입하는 방식은 같으나 기본형과 참조형에 따라 동작하는 방식이 달라짐

 

기본형 vs 참조형 - 기본

  • 자바의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류
  • 기본형을 제외한 나머지 변수는 모두 참조형. 클래스와 배열을 다루는 변수는, 참조형.
  • 기본형 변수는 값을 직접 저장, 참조형 변수는 참조(주소)를 저장
  • 기본형 변수는 산술 연산을 수행할 수 있음, 참조형 변수는 산술 연산을 수행할 수 없음.
  • 기본형 변수는 null을 할당할 수 없음, 참조형 변수는 null을  할당할 수 있음.

기본형 vs 참조형 - 대입

  • 기본형과 참조형 모두 대입 시 변수 안에 있는 값을 읽고 복사해서 전달
  • 기본형은 사용하는 값을 복사해서 전달, 참조형은 참조값을 복사해서 전달(실제 인스턴스가 복사되는 것이 아니고 인스턴스를 가리키는 참조값을 복사해서 전달 - 하나의 인스턴스를 여러 곳에서 참조할 수 있음)
  • 헷갈리면 변수 안에 들어간 값을 생각하면 됨(기본형은 사용하는 값이 참조형은 참조값이 들어 있음, 변수에 어떤 값이 들어 있든 간에 그 값을 복사해서 전달함)

기본형 vs 참조형 - 메서드 호출

  • 메서드 호출 시 기본형은 메서드 내부에서 파라미터 값을 변경해도 호출자의 변수 값에는 영향이 없음
  • 메서드 호출 시 참조형은 메서드 내부에서 파라미터로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경 됨

'항해 99 > Java' 카테고리의 다른 글

Java 기초 9 - 생성자  (1) 2024.01.25
Java 기초 8 - 객체 지향 프로그래밍  (0) 2024.01.25
Java 기초 6 - 메서드  (1) 2024.01.23
Java 기초 5 - 배열  (0) 2024.01.22
Java 기초 4 - 훈련  (1) 2024.01.22