[Java 기본]Section 2 - 기본형과 참조형

우영·2024년 1월 26일

JAVA 기본편

목록 보기
2/13

기본형 vs 참조형 - 시작

자바에서 참조형을 제대로 이해하는 것은 정말 중요하다.
변수의 데이터타입을 가장 크게 보면 기본형과 참조형으로 분류가능

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

기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)가 들어가 있다. 참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 해당 위치로 이동해야 한다.

기본형 vs 참조형 - 기본

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

기본형 vs 참조형 - 계산

  • 기본형은 들어있는 값을 그대로 계산에 사용할 수 있다.
    예) 더하고 빼고, 사용하고 등등, (숫자 같은 것들은 바로 계산할 수 있음)

  • 참조형은 들어있는 참조값을 그대로 사용할 수 없다. 주소지만 가지고는 할 수 있는게 없다. 주소지에 가야 실체가 있다!
    예) 더하고 빼고 사용하고 못함!! 참조값만 가지고는 계산 할 수 있는 것이 없음!

쉽게 이해하는 팁

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

    • 기본형은 자바가 기본으로 제공하는 데이터 타입이다. 이러한 기본형은 개발자가 새로 정의할 수 없다. 개발자는 참조형인 클래스만 직접 정의할 수 있다.
  • 클래스는 대문자로 시작한다. ex) Student

    • 클래스는 모두 참조형이다.

참고

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

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

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

  • 기본형은 실제 사용하는 값만 복사해서 대입하는 것
  • 참조형은 실제 사용하는 객체가 아니라 객체의 위치를 가르키는 참조값만 복사되는 것

기본형과 변수 대입

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);
 }
}

참조형과 변수 대입

package ref;
public class Data {
 int value;
}
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);
 }
}

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

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

대원칙: 자바는 항상 변수의 값을 복사해서 대입한다.
기본형은 변수에 있는 실제 값을, 참조형은 변수에 들어 있는 참조값을 복사해서 대입

기본형과 메서드 호출

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

참조형과 메서드 호출

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);
 changeReference(dataA);
 System.out.println("메서드 호출 후: dataA.value = " + dataA.value);
 }
 static void changeReference(Data dataX) {
 dataX.value = 20;
 }
}

다음 메서드 호출 코드를 보고 어떤 결과가 나올지 먼저 생각해보기
= 둘다 10으로 생각했다.
-> ChangeReference(dataA)
1. dataA의 참조값(x001)을 changeReference 메서드에 복사해서 대입한다.(둘의 참조값은 동일)
2. 참조값을 찾아가서 값을 20을 복사해서 대입한다.
3. x001의 값은 20으로 바뀐다.
4. 그래서 20 20으로 출력한다.

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

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

자꾸 헷갈리는 것

참조형 변수는 참조값(주소값)을 보관한다.

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

package ref;

public class Method1 {

    public static void main(String[] args) {
        Student student1 = new Student();
        initStudent(student1,"학생1", 15,80);
        Student student2 = new Student();
        initStudent(student2,"학생2", 16,80);
        /*student1.name = "학생1";
        student1.age = 15;
        student1.grade = 80;


        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);*/
        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);
    }

}

(첫 번째) 메서드 호출 (주석처리를 한 코드)

(두 번째) 메서드에 객체 전달

  1. initStudent 메서드를 호출
  2. 메서드 로직 실행해서 멤버 변수에 값 설정
  3. Student student1;에 참조값을 저장해서 출력
  • 다른 패키지에 있는 Student를 사용하면 안된다는 뜻

(세 번째) 메서드에서 객체 반환


  • 이 부분을 만들어서 메서드안에 객체를 생성하는 부분도 포함시킴. 이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리함.

createStudent() 메서드 호출 분석

변수와 초기화

변수의 종류

멤버변수: 클래스에 선언

지역변수: 메서드에 선언, 매개변수도 지역변수의 한종류

변수의 값 초기화

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

null

  • 값이 존재하지 않는, 없다는 뜻
package ref;
public class Data {
 int value;
}
  • 앞서 만들었던 Data 클래스이다. 기존 코드를 그대로 유지하면 된다.
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);
 }
}

  1. Data data = null -> null값을 할당함으로써 data변수에는 아직 가리키는 객체가 없다는 뜻
  2. data = ref.Data@x001 -> data = new Data(); 새로운 Data 객체를 생성해서 그 참조값을 data 변수에 할당했다. 이제 data 변수가 참조할 객체가 존재한다.
  3. data = null -> data = null; data에 다시 null을 할당했다. 이렇게 하면 전에 있던 data 인스턴스를 더는 참조할 수 없게 된다.

GC(가비지 컬렉션) - 아무도 참조하지 않는 인스턴스의 최후

  • 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지한다.
    • c와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했다.
    • 하지만 자바는 이런 과정을 자동으로 처리해준다.

아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더이상 사용하지 않는 인스턴스라고 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다.

NullPointerException

  • 주소가 없는 곳을 찾아가서 예외가 발생되는 것
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.value로 인식이 되어 찾아갈 수 없어서 NullPointerException 예외처리가 발생한다.

  • 이렇게 null값이 대놓고 있어서 문제를 파악하기 쉽다. 하지만 멤버 변수가 null인 경우는 주의가 필요하다.

멤버변수와 null

package ref;
public class Data {
 int value;
}
  • 기존의 Data 클래스를 사용한다.
package ref;
public class BigData {
 Data data;
 int count;
}
  • BigData 클래스는 Data data , int count 두 변수를 가진다.
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);
 }
}

정리
1. bigData의 data 멤버 변수는 참조형(Data data)이므로 null로 초기화된다. count 멤버 변수는 숫자(int count) 이므로 0으로 초기화된다.

  • 이 문제를 해결하려면 Data 인스턴스를 만들고 BigData.data 멤버 변수에 참조값을 할당하면 된다.
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);
 System.out.println("bigData.data.value=" + bigData.data.value);
 }
}

정리
bigData.data = new Data();을 넣어줌으로써 밑의 그림처럼 들어간다.

최종정리

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

문제풀이

몰라서 해설 보고 하다가 이해 안되는 부분

  • order.productName = productName; -> 변수명이 왜 같은지 앞에있는 변수명은 메소드 멤버변수이고 = 뒤에 있는 것은 메서드 내에 속하는 지역변수(매개변수)이다.

전체코드

package ref.ex;

    public class ProductOrderMain2 {
        public static void main(String[] args) {
            ProductOrder[] orders = new ProductOrder[3];
            orders[0] = createOrder("두부", 2000, 2);
            orders[1] = createOrder("김치", 5000, 1);
            orders[2] = createOrder("콜라", 2500, 2);

            printOrders(orders);
            int sum = getTotalAmount(orders);
            System.out.println("총 결제 금액: " + sum);
        }
        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 sum = 0;
            for (ProductOrder order : orders) {
                sum += order.price * order.quantity;
            }
        return sum;
        }
    }

부분 코드

static void printOrders(ProductOrder[] orders){
            for (ProductOrder order : orders) {
                System.out.println("상품명: " + order.productName + ", 가격: " + order.price + ", 수량: " + order.quantity);
            }
        }
  • printOrders(orders);로 메서드를 호출할 수 있다.
  • void는 반환값이 없을 때 쓰면 된다.

부분 코드

static int getTotalAmount(ProductOrder[] orders){
            int sum = 0;
            for (ProductOrder order : orders) {
                sum += order.price * order.quantity;
            }
        return sum;
        }
  • int sum을 선언 및 초기화
  • iter(향상된 for문)을 사용해 sum값을 계속 더한다. 그후 sum을 return
  • int sum = getTotalAmount(orders);을 사용해 메서드 호출
  • int sum을 쓰지 않고 -> ctrl + alt + v : 반환값 자동으로 처리
  • System.out.println("총 결제 금액: " + sum); 출력으로 마무리
    두번 보니까 이해가 된다. 하지만 향산된 for문은 아직 익숙치 않다.

문제: 상품 주문 시스템 개발 - 사용자 입력

문제 설명
앞서 만든 상품 주문 시스템을 사용자 입력을 받도록 개선해보자.
요구 사항
아래 입력, 출력 예시를 참고해서 다음 사항을 적용하자.
주문 수량을 입력 받자
예) 입력할 주문의 개수를 입력하세요:
가격, 수량, 상품명을 입력 받자
입력시 상품 순서를 알 수 있게 "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 n = scanner.nextInt();

            ProductOrder[] orders = new ProductOrder[n];
                for (int i =0; i < orders.length; i++){
                    System.out.println( i+1 +"번째 주문 정보를 입력하세요.");
                    scanner.nextLine();
                    System.out.print("상품명: ");
                    String productName = scanner.nextLine();
                    System.out.print("가격: ");
                    int price = scanner.nextInt();
                    System.out.print("수량: ");
                    int quantity = scanner.nextInt();


                    orders[i] = createOrder(productName, price, quantity);
                }

            printOrders(orders);
            int sum = getTotalAmount(orders);
            System.out.println("총 결제 금액: " + sum);
        }
        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 sum = 0;
            for (ProductOrder order : orders) {
                sum += order.price * order.quantity;
            }
        return sum;
        }
    }

이해 안됬던 부분

int n = scanner.nextInt();
scanner.nextLine();

            ProductOrder[] orders = new ProductOrder[n];
                for (int i =0; i < orders.length; 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();

nextInt()는 \n을 남기고 간다. 그러다가 nextLine()을 만나면 nextLine()은 엔터를 기준으로 넘겨버리기 때문에 바로 종료가 된다. 이런걸 방지하기 위해 int n 밑에 nextline()을 넣어서 버퍼를 비우고 for문을 들어가서 또 돌기전에 버퍼를 비우고 요런식으로 작동된다.

0개의 댓글