I/O

반응형
반응형

I/O 입력과 출력을 뜻하는 용어로, 입출력은 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 말한다.

스트림/버퍼/채널

스트림

->

스트림은 흐름이라는 뜻을 가지고 있다.(람다의 스트림과 의미는 같을지도 모르겠지만 전혀 다른 용어이다.)

간단히 말하면 입력에서 출력으로 흐르는 흐름을 뜻한다.
그냥 통로 입력이 출력으로 가는 통로라고 생각해도 될것 같다.

입력에서 출력으로 가기 때문에 단방향으로 데이터 전송이 된다.
왜냐하면 출력에서 입력으로 가는것은 말도 안 되기 때문이다.

만약에, 한 개가 아니라 여러개를 동시에 보낸다고 한다면 어떻게 될까?

이런식으로 큐 형식(FIFO)으로 진행이 된다는 것을 알 수 있다.
즉, 먼저 입력받은것이 가장먼저 입력되는 시스템인거죠.

 버퍼

그렇다면 버퍼는 무엇일까?

일반적인 입출력과 다르게, 한곳에 저장시킨뒤, 한번에 보내는 방식을 뜻한다.

확실히 스트림보다 빠르다는 느낌을 주는 것 같다.

채널

우리가 알고 있듯이 TV채널과 이 채널은 같은 뜻 일까?
채널은 입력 과 출력을 동시에 할 수 있다고 한다.
즉, 비동기적으로 입출력이 가능하다는 이야기 같다.

그림은 동기식처럼 보이지만 기술상의 문제로 비동기처럼 보여주기는 어려울것 같아 이런식으로 표현했다.

InputStream과 OutputSteatm

InputStream과 OutputStream은 모든 바이트 기반 스트림의 조상이다.

입출력을 처리할 수 있는 표준적인  방법을 제공하고 있기 때문에 대상이 달라져도 동일한 방법으로 입출력이 가능하기 때문에,
프로그래밍 하기가 편리하다.

InputStream inputStream = new InputStream() {
     @Override
     public int read() throws IOException {
        return 0;
    }
};

OutputStream outputStream = new OutputStream() {
     @Override
     public void write(int b) throws IOException {
                
    }
};

위에서 확인했듯이 각자의 상황에 맞게 구현하기 위해 추상 클래스로 정의되있다.
input은 총 3가지로 구성되어있다.

public int read()
public int read(byte[] b)
public int read(byte[] b, int off, int len)

output도 비슷하게 되어있다.

public void write(int b);
public void write(byte[] b);
public void write(byte[] b, int off, int len);

이들은 모든 입출력의 최고 조상이기 때문에, 알아두면 좋을 것 같다.

Byte기반 스트림와 문자 기반스트림

바이트는 정보의 기본단위로 1바이트 당 8bit를 뜻합니다.

(참고로 비트는 0과 1로 나타냅니다.)

그렇다는 이야기는 바이트기반 스트림 같은 경우는 문자열이라던지, 등등을 바이트로 쪼개서 전송하는 스트림으로 해석할 수 있을 것 같다.

ByteArrayInput(output)Stream

바이트 기반 Stream중 하나로 바이트 배열로 입출력을 할때 사용된다.

byte[] inSrc = {0,1,2,3,4,5,6,7,8};
byte[] outSrc;
ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
ByteArrayOutputStream output = new ByteArrayOutputStream();

int data;

int data = input.read();
output.write(data);

outSrc = output.toByteArray();
System.out.println("Input => " + Arrays.toString(inSrc));
System.out.println("Output => " + Arrays.toString(outSrc));

1. 입력 스트림을 이용해서 input에 존재하는 데이터를 읽는다.
2. 출력 스트림을 이용해서 그 값을 가져온다.
3. 이것을 무한반복한다.

그런데 무한 반복을 하게 되면 끝이 나지 않게 된다.

예제에서 data = -1이 아니라면 반복을 종료하게 되는데, 그 이유는

int cnt = 0;
  while (true) {
    output.write(input.read());
    cnt++;
    if (cnt > 10) break;
}

이러한 코드로 확인 할 수 있다.

-1이 추가되었다는 것을 확인 할 수 있다.
그래서 결국 다음과 같은 코드로 주워진 배열을 스트림을 통해 전달 되려면,

int data;
while ((data = input.read()) != -1) {
    output.write(data);
}

이런식으로 수정하면 된다.

위 방식은 다음처럼  작성할 수 있다.

 while (input.available() > 0) {
       output.write(input.read());
}

이번엔 버퍼 방식을 이용해서 위를 더 효율적으로 수정해보자.

byte[] inSrc = {1,2,3,4,5,6,7,8};
byte[] buffer = new byte[9];
byte[] outSrc;
ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
ByteArrayOutputStream output = new ByteArrayOutputStream();

input.read(buffer,0,buffer.length);
output.write(buffer,0,inSrc.length);

outSrc = output.toByteArray();
System.out.println("Input => " + Arrays.toString(inSrc));
System.out.println("Output => " + Arrays.toString(outSrc));

버퍼에 1부터 8까지 입력 받은 뒤,
그 값을 outputStream으로 정해진 조건에 따라 바로 리턴되는 방식이다.

input.read(버퍼 배열,초기 인덱스,입력 받을 크기);
output.write(버퍼 배열,초기 인덱스,출력 받을 크기);

다른 방식은 존재하지 않을까?

while (input.available() > 0) {
       input.read(buffer);
       output.write(buffer);
}

위 코드를 설명하자면,
입력 스트림이 허용되는 한, input배열은 buffer크기만큼 읽고, 
그 값을 output으로 buffer의 크기만큼 출력되는 형태를 띈다.

하지만, 위 코드로 버퍼를 이해하는건 조금 부족한것 같습니다. 그래서 이렇게 수정했다.

while (input.available() > 0) {
       int read = input.read(buffer);
        System.out.println(read);
        output.write(buffer,0,read);
}

 

1부터 8까지 준비하고 버퍼는 4개씩 저장되는 형태로 되어있다.
그리고 계속적으로 버퍼에 존재하는 값들을 output으로 옮기고 버퍼를 지우는 방식으로 되어있다.

문자 기반 스트림

바이트 기반 스트림의 조상이 InputStream과 OutputStream인것 처럼 문자 기반 스트림은 Reader와 Writer가 조상이다.

바이트 기반 스트림과의 차이점은
 - 2byte로 스트림을 처리한다.
 - 여러 종류의 인코딩과 자바에서 사용하는 유니코드간의 변환을 자동으로 처리한다.

StringReader, StringWriter

String inputData = "ABCD";
StringReader input = new StringReader(inputData);
StringWriter output = new StringWriter();
int data;

while ((data = input.read()) != -1) {
    output.write(data);
}

System.out.println("Input data => " + inputData );
System.out.println("Output data => " + output.toString() );

보조 스트림

성능을 향상 시키거나, 기능을 보완하기 위해 제공된다.

바이트 기반 스트림의 보조 스트림

FilterInputStream  / FilterOutputStream

바이트 기반 스트림의 조상격이며,

class FilterInputStream extends InputStream {
 
    protected volatile InputStream in;
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }    
  ...
}

코드를 보면 특히한것이 보인다. 
생성자가 protected로 되어있는것을 확인 할 수 있다.
이것이 의미하는 건 이것을 new를 이용해서 사용하는 것은 불가능하고,
무조건 상속을 받아서 사용해야 된다. (커스텀을 하던지 아니면 BufferInputStream처럼 이미 만들어진 보조 스트림을 사용해야된다.)

대표적인 예로 BufferInputStream을 보자.

BufferInputStream  / BufferOutputStream

public
class BufferedInputStream extends FilterInputStream {
    ... 
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }


    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
...
}

버퍼를 이용한다는 것을 알 수 있다.
FilterInputStream 생성자의 접근지시자는 protected이지만, 상속받은 BufferedInputStream은 public이다.

그렇다면 문자 기반 스트림은 어떨까?

간단히 Input -> Reader 이 되면되고, output -> Writer로 변경시켜주면 된다.

이밖에도 FileInputStream,FileReader, LineInputStream등 여러가지 보조 스트림을 사용할 수 있다.

표준 입출력 System.out, System.in, System.err

- 콘솔로 부터의 데이터 입력을 뜻한다.
- 자바 어플리케이션이 실행과 동시에 생성하는 코드를 제공하기 때문에 별도의 스트림을 만들 필요가 없다.

다음은 System코드의 일부입니다.

public final class System {
public static final InputStream in = null;
public static final PrintStream out = null;
public static final PrintStream err = null;

    public static void setIn(InputStream in) {
        checkIO();
        setIn0(in);
    }
    
    public static void setOut(PrintStream out) {
        checkIO();
        setOut0(out);
    }
    
     public static void setErr(PrintStream err) {
        checkIO();
        setErr0(err);
    }
    
private static native void setIn0(InputStream in);
private static native void setOut0(PrintStream out);
private static native void setErr0(PrintStream err);

따라가다보면 자바 코드가 아닌 c나 c++코드로 되었다는 것을 알 수 있다.

int input;
while ((input = System.in.read()) != -1) {
     System.out.println("input : " + input + ", (char)input : " + (char) input);
}

이 코드는 값을 하나씩 읽어서 프린트를 해주는 간단한 코드다. 이 코드를 이용하면 

이러한 결과를 알 수 있다.
여기서 주의해야 점은 input의 값이 -1일때 프로그램이 종료되게 만들면 된다.
이것을 EOF(End Of File)이라고한다.
window : ctr+z를 누르면 나가진다.
맥: command + d를 누르면 된다.

하지만 빈칸을 제거하는것이 불편하다고 한다.
왜냐하면 일일이 제거해줘야 하기 때문이다.
그래서 bufferedReader를 이용해서 readLine()을 통해 라인 단위로 데이터를 입력 받는다.
그나마 자바1.5부터 Scanner등이 추가되면서 자바에서도 콘솔로 입력받거나 출력받는 것이 편해졌다.

표준입출력의 대상 변경

원래는 콘솔로 입력을 받거나 출력을 받을 수 있는데,
이것을 바꾸는 방법이 바로

public static void setIn(InputStream in) {
     checkIO();
     setIn0(in);
}
    
public static void setOut(PrintStream out) {
     checkIO();
     setOut0(out);
}
    
public static void setErr(PrintStream err) {
     checkIO();
     setErr0(err);
}

이것으로 이용하면 된다.

그럼 파일 정보를 읽어서 콘솔로 프린트를 할 수는 없을까?

첫 번째 방법

FileInputStream fis = null;
String result = "";
try {
     fis = new FileInputStream("test.txt");
     System.setIn(fis);
      int data;
      while ((data = fis.read()) != -1) {
       result += (char)data;
            }
   } catch (IOException e) {
      e.printStackTrace();
    }
System.out.println(new String(result.getBytes("ISO-8859-1")));

공부하다 특이점을 찾게 되었는데,
PrintStream은 일반 클래스로 작성되어 있지만,
InputStream은 추상 클래스로 작성 되어있다.

그래서 setIn에 inputStream을 넣기로 했다. 
근데 문제가 발생했다.
바로 인코딩 문제였다.  

예상 결과 값 => 한글

 

자바 java에서 텍스트파일을 읽을 때 한글이 깨지는 현상에 대한 처리

출처 - http://shonm.tistory.com/entry/%EC%9E%90%EB%B0%94-TEXT-%ED%8C%8C%EC%9D%BC-...

blog.naver.com

 

인코딩을 ISO_8859_1 으로 바꾸니 정상적으로 출력되는 것을 확인했다.

위에는 인코딩 값을 문자열로  작성 했지만, 그것보다 아래처럼 작성(상수)하는 것이 더 좋아 보인다.
왜냐하면 타입세입하기 때문이다.

System.out.println(new String(result.getBytes(StandardCharsets.ISO_8859_1)));

 

두 번째 방법

이 방법은 IDE에서 제공되는 콘솔에서는 불가능하고,
터미널에서 사용할 수 있는 방법이다.

java 자바파일 > 파일명
자바 파일에서 파일로 입력 (수정)

java 자바파일 >> 파일명
자바 파일에서 파일로 입력 (추가)

java 자바파일 < 파일명
파일에서 자바파일로 입력

File

RandomAcessFile

 - 하나의 클래스로 파일에 대한 입출력을 모두 할 수 있도록 되어있다.

소스를 읽어보니

    private RandomAccessFile(File file, String mode, boolean openAndDelete)
        throws FileNotFoundException
    {
        String name = (file != null ? file.getPath() : null);
        int imode = -1;
        if (mode.equals("r"))
            imode = O_RDONLY;
        else if (mode.startsWith("rw")) {
            imode = O_RDWR;
            rw = true;
            if (mode.length() > 2) {
                if (mode.equals("rws"))
                    imode |= O_SYNC;
                else if (mode.equals("rwd"))
                    imode |= O_DSYNC;
                else
                    imode = -1;
            }
        }
        if (openAndDelete)
            imode |= O_TEMPORARY;
        }
     }   

r => O_RDONLY  (읽기만 가능) READ ONLY 
rw => O_RDWR (파일에 읽기와 쓰기 가능) READ AND WRITE 
기본적으로 이렇게 2가지가 사용되고,
rws => O_SYNC 
rw와 기능이 같다. 파일의 메타 정보도 같이 포함 된다고 한다.
rwd => O_DSYNC
역시 rw와 기능이 같다. 파일의 내용만 출력하게 된다.

rw와의 차이점은 출력내용이 지연없이 바로 쓰이게 된다고 한다.

또한 모드 중에는

O_TEMPORARY라고 존재하는데,
열렸거나 지워지면 이 모드가 실행된다고 한다.

이렇게 상수로 만들어졌는데 문자열로 입력이 되는게 아쉽다.ㅜㅜ

하나씩 사용해보자.

RandomAccessFile raf = new RandomAccessFile("test.txt", "r");
String data;
while ((data = raf.readLine()) != null) {
     System.out.println(data);
}

이런식으로 사용하면 되는데,
위에서 봤듯이 r같은 경우 읽기만 가능한 모드입니다.
따라서 여기에 입력을 하게 되면 어떻게 될까요?
강제로 

raf.writeChars("bubble");

작성해보면,

모드가 r이어서 그런가...

이것을 rw로 바꾸는 순간,

RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
String data;
while ((data = raf.readLine()) != null) {
     System.out.println(data);
}
raf.writeChars("bubble");

나오지 않는다.

그렇다면 출력은?

맨뒤에 출력이 된다는 것을 확인 할 수 있다.

신기하군....

만약, rw일때 파일이 없다면 생성해준다. 하지만 r일경우에는 에러가 발생한다.

RandomAccessFile raf = new RandomAccessFile("play.txt", "rw");
String data;
raf.writeChars("bubble");
raf.seek(0);
raf.writeChars("1");
while ((data = raf.readLine()) != null) {
     System.out.println(data);
}

이것의 결과는 과연 무엇일까?

1. bubble1
2. 1
3. ubble
4.1bubble

raf.seek의 역할은 파일 포인터의 위치를 변경시키는 것이다.

더보기

파일 포인터에 대해 학습해보자.

위 소스를 이용해서 0번 부터 10번까지 추가해보고,
아래는 변경하지 말고 해보자.

포인터는 ^로 표현했다.

포인터 : 0

파일 내용

^1^u^b^b^l^e

포인터 : 1

^^1u^b^b^l^e

포인터 : 2

^b^1^b^b^l^e

포인터 : 3

^b^^1b^b^l^e

포인터 : 4

^b^u^1^b^l^e

 포인터 : 5

^b^u^^1b^l^e

 포인터 : 6

^b^u^b^1^l^e

 포인터 : 7

^b^u^b^^1l^e

포인터 : 8

^b^u^b^b^1^e

 포인터 : 9

^b^u^b^b^^1e

 포인터 : 10

^b^u^b^b^l^1

즉, 포인터가 있는 부분에서 작성이 되고, 그 뒤로 출력이 되는 형태라고 할 수 있다.
번외로 short, int, long을 확인해보자.
byte,char은 같은 결과가 나온다.

포인터 : 0
작성 인트 : 1(int)

^^^^$b^b^l^e

$ : 숫자 각자 기호로 표현되어서 출력된다.

포인터 : 0
작성 롱 : 1(long)

 

포인터 : 0
작성 쇼트 : 1(short)

사용해보기

RandomAccessFile raf = new RandomAccessFile("play.txt", "rw");
raf.writeChars("한글이 되네..");
raf.seek(0);
try {
      while (true) {
        System.out.println(raf.readChar());
     }
 } catch (EOFException e) {

}

파일에 작성하구, 프린트를 하는 소스를 만들었습니다.

raf.seek(0)을 한 이유는
포인터를 강제로 처음으로 돌려야 정상적으로 출력되기 때문입니다.

스샷을 찍었는데 실수로 커서가 찍혔다.

File

여기서 알아야할 것은 절대 경로와 상대 경로라는 개념이다.

예를들어, 

다음과 같은 파일들이 존재한다고 가정하자.

또 루트에는 

이런 파일들이 존재한다.
우리가 테스트할 파일명은 test.txt다.
test.txt같은 경우 homework13에도 존재하고, root에도 존재한다.

그러면,

File file = new File("test.txt");

이렇게 인스턴스를 생성하게 된다면 경로가 어떻게 지정이 될까?

root에 존재하는 test.txt정보가 나오는것을 확인 할 수 있다.
그러면 homework13에 존재하는 test.txt를 호출하는 것은 불가능할까?

바로 경로를 이용하는 방법이다.

절대 경로 

그 파일이 가지고 있는 경로를 루트를 기준으로 작성하면 된다.
그러니까,

/Users/개인정보/Desktop/intelij/whiteship/src/main/java/study/whiteship/homework13

+ /test.txt를 추가해주면 된다.

이해를 위해 위 사진과 비교 해보자.
첫번째 whiteship 이후로 디렉토리가 많이 존재하는 것을 확인 되었다.

그러면
root에 존재하는 test.txt와
src는 같은 위치에 존재한다는 것을 알 수 있다.

즉, root => 여기서는 Users 부터 그 파일까지 경로가 이어진것을 절대 경로라고 부른다.

근데 우리는 저걸 일일이 다 입력하는것은 쉽지 않다.

그래서 상대 경로라는 것이 필요하다.

상대 경로

절대 경로가 root를 기준으로 작성되어졌다면, 상대 경로는 root가 아닌 자기 자신 즉,test.txt를 기준으로 작성되어졌다,
그러면 어떻게 자기자신을 기준으로 둘까?

/ : 루트
./ : 현재 위치
../ : 현재 위치의 상단 폴더

먼저 root-test.txt부터 확인하자.
그러면 test.txt도 root에 존재하는 파일이기 때문에 

./test라고 작성하면 되는 걸까?

/Users/개인 정보/Desktop/intelij/whiteship/./test.txt

맞아.

그러면 homework13의 test.txt는 어떻게 호출할까?

확인 결과 pathname으로 지정할 경우 현재 파일이 root이기 때문에 이 방법으로는 불가능하다.
그러면 방법이 없는 걸까?
포커스를
homework13으로 마춰두고

File file = new File("/Users/개인정보/Desktop/intelij/whiteship/src/main/java/study/whiteship/homework13","./test.txt");

넣어주니 되었다. 하지만 이것도 상대 경로가 아닌것은 함정
내가 test.txt로 포커스를 맞추는건 불가능하다는 것을 알 수 있다.

결국. 상대경로를 사용할 수 있는 조건은
파일(test.txt)을 직접적으로 사용할 수 있을때 의미가 아닐까 생각이 든다.
 ./test.txt에서 ./는 생략이 가능하다고 한다.

이대로 끝내는건 아쉬우니까,
test.txt의 상대경로를 작성해보자.

./test.txt =>
./가 현재 폴더이니 homework13혹은, 첫번째 whiteship라는걸 알수 있고,

../test.txt => 
../ 상위 폴더이니 두 번째 whiteship혹은 intelij라는 걸 알 수 있다.
./../test.txt == ../test.txt

../../test.txt 상위의 상위 => study,Desktop이라는 걸 알 수 있다.

참고로 루트의 상대경로는 작성이 가능하다.

File file = new File("../");

이건 intelij?

맞네

그러면 파일을 new File을 통해 생성하구  그 파일에 내용을 작성하구 
파일 내용을 읽어보자.

    public static void main(String[] args) throws IOException {
        File file = new File("study.txt");
        if(!file.exists()) {
            if (file.createNewFile()) {
                System.out.println("파일이 생성되었습니다.");
            }
        }
        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
        BufferedReader br = new BufferedReader(new FileReader(file));

        bw.write("hello");
        bw.newLine();
        bw.write("선장님 감사합니다!!");
        bw.flush();

        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }

        if(file.exists()) {
           if(file.delete()) {
               System.out.println("파일이 삭제되었습니다.");
           }
        }
    }

파일이 존재하지 않는다면, 파일을 만들구
작성하구,
콘솔에 뿌려주기로 했다.

공부하다 새롭게 안 사실은 flush를 해주지 않으면 반영이 되지 않는 점이다.

결과

직렬화

 - 객체를 데이터 스트림으로 만드는 것을 뜻한다.

직렬화 : ObjectOutputStream
역 직렬화 : ObjectInputStream

일단 직렬화를 시켜보자.

여러 스트림을 거쳐서 file로 만들었다.
그리고 작성해보자.!

UserInfo u1 = new UserInfo("JavaMan","1234",30);
UserInfo u2 = new UserInfo("JavaWoman","4321",26);
List<UserInfo> list = List.of(u1, u2);

정보들을 이렇게 준비하고,
이 정보들을 file에 넣어보자!

out.writeObject(u1);
out.writeObject(u2);
out.writeObject(list);

저번에 배운 try-catch-resource를 이용해서 자동으로 반환하게 만들어줬다.

try(ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)))) {
      UserInfo u1 = new UserInfo("JavaMan","1234",30);
      UserInfo u2 = new UserInfo("JavaWoman","4321",26);
      List<UserInfo> list = List.of(u1, u2);
      out.writeObject(u1);
      out.writeObject(u2);
      out.writeObject(list);
      System.out.println("직렬화가 끝났습니다.");
}

그럼 직렬화 된 내용이 파일에 저장된다.

¬ísr#study.whiteship.homework13.UserInfo#‹S‡ gתIageLnametLjava/lang/String;Lpasswordq~xptJavaMant1234sq~t	JavaWomant4321srjava.util.CollSerWŽ«¶:¨Itagxpwq~q~x

글씨가 마니 깨지기는 했지만ㅜㅜ;
이런식으로 저장된다는 것을 알 수 있다.

그냥 객체를  사용하면 될까?
결론부터 말하면 틀렸다.
바로

implements Serializable

를 해줘야 된다. 이것을 안해주면 직렬화가 정상적으로 동작하지 않게 된다.

이제 직렬화를 해줬으니, 
이것을 역 직렬화를 이용해서 다시 객체로 만들어보자.

file에서 콘솔로 입력이 되어지는 형태로 작성했다.

UserInfo u1 = (UserInfo) in.readObject();
UserInfo u2 = (UserInfo) in.readObject();
List list = (List) in.readObject();

값들을 꺼내고,

System.out.println(u1);
System.out.println(u2);
System.out.println(list);

그것들을 프린트 했다.

그러면 역 직렬화한 결과를 확인 할 수 있게 된다.

주의 해야 될점이 있다.
한 번 직렬화 시킨건 파일 내용이 변경되는 순간,
역 직렬화가 불가능해진다.

간단히

transient String name;

 를 추가해줬다.
transient는 조금이따 소개하도록 하겠다.

그러면,

이런 에러를 확인 할 수 있다.

이럴때 두 가지 방법이 있는데
하나는 다시 직렬화를 시켜주는 방법이 있겠고,
다른 하나는 버전을 추가해주면 된다.

static final long serialVersionUID = 2561232655067109290L;

뒤 숫자는 적절히 위 에러에서 나온 값을 사용했다.

그러면,

정상적으로 나오는것을 알 수 있다.

즉, 위 값으로 직렬화가 되었다는 것을 의미한다,

transient

를 붙여주면 그 값은 무시되는것을 알 수 있다.

 

출처 : 자바의 정석

반응형

'프로그래밍 언어 > 자바' 카테고리의 다른 글

제네릭  (0) 2021.02.21
멀티쓰레드 프로그래밍  (0) 2021.02.13
I/O  (0) 2021.02.11
어노테이션  (0) 2021.01.31
ENUM  (0) 2021.01.24
예외 처리  (0) 2021.01.14

댓글(0)

Designed by JB FACTORY