Primitive Type 정의
데이터가 메모리에 어떻게 저장되고
프로그램에서 어떻게 처리되어야 하는지를
명시적으로 알려주는 역할을 수행하는 기본 자료형
값 할당 시 변수의 주소값
에 값
이 저장되는 데이터 타입
값 할당 시
JVM Runtime Data Area -> Stack
영역 순 으로 저장
Primitive Type 의 종류
Division | Type | Default | Area | Size (byte) | 비고 |
---|---|---|---|---|---|
정수형 | byte | 0 | -128 ~ 127 | 1 | |
short | 0 | -32,768 ~ 32,767 | 2 | ||
int | 0 | -2,147,483,648 ~ 2,147,483,647 | 4 | ||
unsigned int | 0 | 0 ~ 4,294,967,295 | 4 | java 8 이상부터 가능 | |
signed long | 0L | -2^63 ~ 2^63 -1 | 8 | ||
unsigned long | 0L | 0 ~ 2^64 - 1 | 8 | java 8이상부터 가능 | |
실수형 | float | 0.0f | (3.4 x 10^-38 ) ~ (3.4 x 10^38) 의 근사값 | 4 | |
double | 0.0 | (1.7 x 10^-308 ) ~ (1.7 x 10^308) 의 근사값 | 8 | ||
문자형 | char | '\u0000' | 0(\u0000) ~ 65,535(\uffff) | 2 | |
논리형 | boolean | false | True, False | 1 |
소수 부분이 없는 수
정수형 데이터 타입을 결정할때 데이터의 최대 크기를 고려해야 함
만약 해당 타입이 표현할 수 있는 범위를 벗어난 데이터를 저장한다면
overflow가 발생
해 전혀 다른 값이 저장될 수 있기 때문
package me.basic.step2.primitive;
public class OverflowExample {
public static void main(String[] args) {
byte num1 = 127;
byte num2 = -128;
num1++;
num2--;
System.out.println( "num1 of value : " + num1 );
System.out.println( "num2 of value : " + num2 );
}
}
// 결과값
// > 정수형 타입 오버플로우 발생
num1 of value : -128
num2 of value : 127
Process finished with exit code 0
실수형 타입 | 지수의 길이 | 가수의 길이 | 유효 자릿수 |
---|---|---|---|
float | 8 비트 | 23 비트 | 소수 부분 6자리까지 |
double | 11 비트 | 52 비트 | 소수 부분 15자리까지 |
package me.basic.step2.primitive;
public class LiteralExample {
public static void main(String[] args) {
float num;
num = 12.34; // 컴파일 에러 발생
System.out.println( num );
}
}
왜 컴파일 에러가 났을까?
이 에러의 원인은 자바 컴파일러가 12.34라는 리터럴에 double 타입을 부여했기 때문이다.
그래서 num = 12.34l라는 명령문은 double 타입의 값을 float 타입의 변수에 대입하는 명령문이 된 것이고, double 타입의 값을 float 타입의 변수에 넣으면 데이터의 정확도에 손상이 갈 수 있기 때문에 이런 컴파일 에러가 발생한 것이다.
이런 문제에 부딪히지 않기 위해서 우리는 리터럴 타입에 대해 알아야 하는데, 리터럴 타입은 표기 방법에 의해 결정된다.
정수형 리터럴
정수형 리터럴은 일반적인 숫자( 정수 데이터 )를 의미 함.
표현할 수 있는 방법
소수점이 없는 10진수 리터럴은 int타입으로 간주 됨.
이때 주의할 점은 0으로 시작되는 정수는 10진수처럼 보여도 8진수로 해석됨.
컴퓨터는 바이트 단위로 데이터를 처리하기 때문에 10진수보다 16진수로 정수를 표기하는것이 많아 그럴때는 10부터 15까지 숫자를 A,B,C,D,E,F or a,b,c,d,e,f로 표시, 제일앞에 0x나 0X를 붙이면 된다.
0x또는 0X로 시작하는 정수 리터럴은 16진수로 취급
10부터 15까지 숫자는 A,B,C,D,E 또는 a,b,c,d,e로 표시
0x30A1
package me.basic.step2.primitive;
public class LiteralExample2 {
public static void main(String[] args) {
System.out.println( 120 );
System.out.println( 024 );
System.out.println( 0x30A1 );
System.out.println( 0x0030a1 );
}
}
< 결과값 >
120
20
12449
12449
public class LiteralExample {
public static void main(String[] args) {
// 정수형
// byte , short , int , long
// 10진수
int i1 = 10;
System.out.println( "i1 : " + i1 );
// 2진수, 8진수, 16진수
int i2 = 0b1010;
System.out.println( "i2 : " + i2 );
int i3 = 030;
int i4 = 0xA4;
System.out.println( "i3 : " + i3 );
System.out.println( "i4 : " + i4 );
byte s1 = 10;
System.out.println( "s1 : " + s1 );
byte s2 = 127;
System.out.println( "s2 : " + s2 );
long l1 = 10;
// long 은 int 보다 커서 15 = l1 이라는 공식이 성립 안됨
System.out.println( "l1 : " + l1 );
// 형변환
// 묵시적 형변환 : 작은 자료형 -> 큰 자료형
// 명시적 형변환 : 작은 자료형 -> 작은 자료형 - 변환형에 이름 기입
int i5 = (int)l1; // 이때 (int)로 long을 변환 시켜 준다면 이 공식은 성립 함
System.out.println( "i5 : " + i5 );
long l2 = i5;
System.out.println( "l2 : " + l2 );
// char <-> int
char c1 = 'A';
System.out.println( "c1 : " + c1 );
// ascii 코드 - 영문자, 숫자를 저장할 때 사용하는 내부 코드
// 유니코드 - 영문자 이외의 문자를 저장할 때 사용하는 내부코드
System.out.println( "(int)c1 : " + (int)c1 );
// char 상태로 전환하면 그 값에 따른 B로 바뀜
int c2 = 'A' + 1;
System.out.println( "(char)c2 : " + (char)c2 );
}
}
< 결과값 >
i1 : 10
i2 : 10
i3 : 24
i4 : 164
s1 : 10
s2 : 127
l1 : 10
i5 : 10
l2 : 10
c1 : A
(int)c1 : 65
(char)c2 : B
정수 리터럴의 마지막에 대문자L이나 소문자l을 쓰면 long타입으로 간주
package me.basic.step2.primitive;
public class LiteralExample2 {
public static void main(String[] args) {
long num = 1234567890123L;
System.out.println( num );
}
}
< 결과값 >
1234567890123
만약 위 예제에서 L자를 빼면?
java: integer number too large: 1234567890123
위 결과와 같이 컴파일 에러가 발생한다.
long 타입의 리터럴 표기 방법이 따로 있다면, byte타입이나 short 타입 리터럴 표기 방법도 따로 있을까?
byte나 short 타입을 위한 리터럴 표기 방법은 따로 없다.
실수형 리터럴
일반적인 실수 표현 방식
지수를 이용한 표현 방식
간단한 float 형을 표현
double 형을 명시한 큰 실수 데이터를 표현
double타입 리터럴에서는 정수부나 소수부 중 한쪽이 0일 경우에 생략가능,
하지만 이때 소수점까지 생략하면 안된다, 그렇게 하면 정수 리터럴과 구분이
되지 않는다.
12.0과 동일
0.025와 동일
12 x 10**10 을 표현하는 부동소수점 리터럴
부동소수점 타입은 아주 큰 수나 작은 수의 표현에도 많이 사용, 지수와 기수로 표기하는 것이 편리할 때도 있음, 그럴때는 기수를 먼저 쓰고, 그 다음에 대문자 E나 소문자 e를 쓰고, 그다음에 지수를 사용하면 됨
부동소수점 리터럴도 정수 리터럴처럼 16진법의 지수, 기수로 표현
이때는 E나 e대신 P나 p를 써야 함
package me.basic.step2.primitive;
public class LiteralExample2 {
public static void main(String[] args) {
System.out.println( "12.025 : " + 12.025 );
System.out.println( "12e3 : " + 12e3 );
System.out.println( "12e-3 : " + 12e-3 );
System.out.println( "0xA1.27p5 : " + 0xA1.27p5 );
}
}
< 결과값 >
12.025 : 12.025
12e3 : 12000.0
12e-3 : 0.012
0xA1.27p5 : 5156.875
public class LiteralExample2 {
public static void main(String[] args) {
float num = 12.34f;
System.out.println( num );
}
}
< 결과값 >
12.34
문자 리터럴
작은 따옴표로 묶은 하나의 문자는 char 타입 리터럴
char 타입 리터럴 예
Escape Sequence | 의미 | Unicode |
---|---|---|
\b | 백스페이스 | 0x0008 |
\t | 수평 탭 | 0x0009 |
\n | 줄 바꿈 문자 | 0x000a |
\f | 새 페이지 문자 | 0x000c |
\r | 리턴 문자 | 0x000d |
\" | 큰따옴표 | 0x0022 |
\' | 작은따옴표 | 0x0027 |
\ | 백슬래쉬 | 0x005c |
\8진수 | 8진수에 해당하는 Unicode 문자. 예) \8, \42, \377 | 0x0000 ~ 0x00ff |
논리형 리터럴
public class LiteralEx04
{
boolean b1 = true;
System.out.println( b1 );
}
변수 정의
변수의 선언
변수의 초기화
char c = 'a';
int i = 7;
double interestRate = 0.05;
변수의 이름
변수의 이름은 '식별자( identifier )' 의 일종으로 규칙은 다음 과 같다.
변수의 선언만 하는 방법
int num; // 변수의 선언
System.out.println( num ); // 오류 발생
num = 20; // 변수의 초기화
System.out.println( num ); // 20
잘못된 변수 선언 예시
int 1stPrizeMoney; // 첫 글자가 숫자
double super; // 키워드
int #ofComputer; // 허용되지 않는 기호
식별자 이름 작성 관계
종류 | 사용 방법 | 예 |
---|---|---|
클래스명 | 각 단어의 첫글자는 '대'문자로 | StaffMember, ItemProducer |
변수명, 메소드명 | 소문자로 시작되어 2번째 단어의 첫 글자는 대문자로 한다 | width, payRate, acctNumber, getMonthDays(), fillRect() |
상수 | 상수는 모든 글자는 '대' 문자로 | MAX_NUMBER |
프로그램상에서 사용되는 변수들은 사용 가능한 범위를 가짐, 그 범위를 scope라고 한다.
변수가 선언된 블록이 그 변수의 사용범위이다.
package me.basic.step2.primitive;
public class ValableScopeExample
{
int globalScope = 10; // 인스턴스 변수
public void scopeTest(int value)
{
int localScope = 10;
System.out.println(globalScope);
System.out.println(localScpe);
System.out.println(value);
}
}
클래스의 속성으로 선언된 변수 globalScope의 사용 범위는 클래스 전체
매개변수로 선언된 int value는 블록 바깥에 존재 하지만, 메서드 선언부에 존재하므로 사용범위는 해당 메소드 블록내 이다
메소드 블록내에서 선언된 localScope 변수의 사용범위는 메소드 블록내
public class ValableScopeExample
{
int globalScope = 10; // 인스턴스 변수
public void scopeTest( int value )
{
int localScope = 10;
System.out.println( globalScope );
System.out.println( localScope );
System.out.println( value );
}
public static void main(String[] args)
{
System.out.println( globalScope ); // error
System.out.println( localScope ); // error
System.out.println( value ); // error
}
}
같은 클래스 안에 있는데 globalScope 변수를 사용할 수 없다.
main은 static한 메소드이다. static한 메서드에서는 static 하지 않은 필드를 사용할 수 없다.
같은 클래스 내에 있음에도 해당 변수들을 사용할 수 없다.
main 메소드는 static 이라는 키워드로 메소드가 정의되어 있다.
static 한 필드( 필드 앞에 static 키워드를 붙힘 )나, static한 메소드는 class 가 인스턴스화 되지 않아도 사용할 수 있다.
public class ValableScopeExample
{
int globalScope = 10;
static int staticVal = 7;
public void scopeTest( int value )
{
int localScope = 20;
}
public static void main(String[] args)
{
System.out.println( staticVal );
}
}
static한 변수는 공유됨
package me.basic.step2.primitive;
public class ValableScopeDisplay {
public static void main(String[] args)
{
ValableScopeExample v1 = new ValableScopeExample();
ValableScopeExample v2 = new ValableScopeExample();
v1.globalScope = 20;
v2.globalScope = 30;
System.out.println( v1.globalScope );
System.out.println( v2.globalScope );
System.out.println( ValableScopeExample.staticVal );
}
}
golvalScope 같은 변수(필드)는 인스턴스가 생성될때 생성되기 때문에 인스턴스 변수라고 한다.
staticVal 같은 static한 필드를 클래스 변수라고 한다.
클래스 변수는 레퍼런스.변수명 하고 사용하기 보다는 클래스명.변수명으로 사용하는것이 더 바람직하다
VariableScopeExam.staticVal
타입변환 = 형변환 (cast)
어떤 자료형의 값을 -> 다른 자료형의 값으로 바꾸어 주는 연산
반환되는 값의 왼쪽에서 원하는 타입을 소괄호로 둘러싸서 적어주면 됨
( 새로운 자료형 ) 수식;
예를들어 int형 변수 x가 가지고 있는 값을 -> double로 형변환하여 y에 대입하려면
y = (double) x;
자동적인 형변환
java는 필요할 떄마다 자동적으로 형변환을 수행 함.
수식에서 서로 다른 자료형이 등장하면 java compiler는 그 중에서 가장 큰 타입으로 자동적으로 변환
// 정수 2가 2.0으로 변환된 후에 3.5와 더해져서 5.5로 계산 됨
double d = 2 + 3.5
축소 변환 ( narrowing conversion )
더 작은 크기의 자료형에 값을 저장하는 형변환
실수형 변수를 정수형 변수에 저장하는 것
이 변환은 정밀한 숫자나 큰 숫자를 나타내는 정보를 잃을 가능성이 있다.
// i에는 12만 저장
int i = (int) 12.5;
위 예에서 소수점 이하는 사라진다.
그러므로, 축소 변환을 할때는 자료를 잃을 가능성 때문에 항상 주의!!
확대 변환 ( widening conversion )
더 큰 크기의 변수로 값을 이동하는 반환
확대 변환은 '안전한 변환'
// 정수 100이 변수 d에 100.0으로 형변환되어 저장
double d = (double) 100;
package me.basic.step2.primitive;
public class TypeConversion {
public static void main(String[] args) {
int i;
double f;
// 피연산자가 정수이므로 정수 연산으로 계산되어 1
// 이것이 double형 변수로 대입되므로 올림 변환이 발생하여 1.0이 f에 저장됨
f = 5 / 4;
System.out.println( "f : " + f );
/*
(double) 5 / 4에서는 먼저 형변환 연산자가 우선 순위가 높기 때문에 먼저 실행,
정수 5가 부동소수점수 5.0으로 변환됨 5.0 / 4는 피연산자 중 하나가 double형이므로 4도 double형
으로 자동 형변환되고 5.0 / 4.0 으로 계산되어서 1.25가 수식의 결과값이 됨,
따라서 1.25가 변수 f에 저장
*/
f = (double) 5 / 4;
System.out.println( "f : " + f );
/*
5 / (double)4 에서도 두번째 피연산자가 double형으로 변환되면 나머지 피연산
자도 double형으로 되어서 전체 수식의 결과값이 double형이 됨
*/
f = 5 / (double)4;
System.out.println( "f : " + f );
/*
두 개의 피연산자가 모두 double형으로 변환됨
*/
f = (double) 5 / (double) 4;
System.out.println( "f : " + f );
/*
모두 int형으로 변환되어 소수점 제거 후 1 + 1 = 2가 저장됨
*/
i = (int)1.3 + (int)1.8;
System.out.println( "i : " + i );
}
}
f : 1.0
f : 1.25
f : 1.25
f : 1.2,
Process finished with exit code 0
메모리 구조
클래스 변수( static variable )이 저장되는 영역
JVM은 특정 클래스가 사용되면 클래스 파일(.class)를 읽어들여, 해당 클래스에 대한 정보를 메소드 영역에 저장
모든 인스턴스 변수가 저장되는 영역
new 키워드를 사용하는 인스턴스의 정보
메모리의 낮은주소 -> 높은 주소의 방향으로 할당
메소드가 호출될떄 스택 프레임이 저장되는 영역
메소드가 호출되면, 관계되는 지역변수와 매개변수를 스택 영역에 저장
메소드 호출이 완료되면 소멸됨
스택 영역에 저장되는 메소드의 호출 정보를 스택 프레임( stack frame )이라고 한다.
스택 영역은( push )로 데이터를 저장, 팝( pop ) 동작으로 데이터를 인출
스택은 후입선출( LIFO, Last-In First-Out ) 방식으로, 가장 늦게 저장된 데이터가 가장 먼저 인출됨.
스택 영역은 메모리의 낮은주소 <- 높은주소 방향으로 할당 됨.
배열
같은 타입의 변수들 모임.
자바에서 배열은 객체이다.
먼저 참조 변수를 선언하고 이어서 객체를 생성한다.
int[] numbers; // 배열 참조 변수 선언
배열 참조 변수를 선언했다고 해서 배열이 생성된 것은 아님, 자바에서는 배열도 객체이므로 반드시 new 연산자를 사용
// 위의 배열 참조변수 객체 생성
numbers = new int[6];
// 실수 배열
float[] distances = new float[20];
// 문자 배열
char[] letters = new char[50];
package me.basic.step2.primitive;
import java.util.Scanner;
public class ArrayExam {
public static void main(String[] args) {
final int STUDENTS = 5;
int total = 0;
Scanner sc = new Scanner( System.in );
int[] scores = new int[STUDENTS];
for( int i = 0; i < STUDENTS; i++ )
{
System.out.print( "성적을 입력하시오 : " );
scores[i] = sc.nextInt();
}
for( int i = 0; i < STUDENTS; i++ )
{
total += scores[i];
}
System.out.println( "\n 평균 성적은" + total / STUDENTS );
}
}
성적을 입력하시오 : 100
성적을 입력하시오 : 70
성적을 입력하시오 : 50
성적을 입력하시오 : 60
성적을 입력하시오 : 80
평균 성적은72
Process finished with exit code 0
// 잘못됨
int matrix[5] = {1, 2, 3, 4, 5};
// 잘못됨
int matrix[5];
배열의 초기화
중괄호를 사용하여 배열 원소의 초기값을 적어 넣음
package me.basic.step2.primitive;
public class ArrayExam2{
public static void main(String[] args) {
int[] numbers = { 10, 20, 30 };
// 일반 for문 방식
for( int i = 0; i < numbers.length; i++)
{
System.out.println( numbers[i] );
}
// for-each Loop 방식 - jdk 1.5 버전 부터 사용
for( int value : numbers )
{
System.out.println( value);
}
}
}
10
20
30
10
20
30
Process finished with exit code 0
배열의 첫 번째 원소부터 마지막 원소의 값을 꺼내서 처리하는 경우라면 for-each 루프가 훨씬 사용하기 쉬움.
배열의 크기에 신경쓰지 않아도 되고 인덱스 변수를 생성할 필요도 없음.
for-each를 사용할 수 없는 경우
import java.util.Scanner;
public class ArrayExam3 {
public static void main(String[] args) {
int total = 0;
Scanner sc = new Scanner( System.in );
System.out.print( "배열의 크기를 입력 : ");
int size = sc.nextInt();
int[] scores = new int[size];
for( int i = 0; i < scores.length; i++ )
{
System.out.print( "성적을 입력하시오 : " );
scores[i] = sc.nextInt();
}
for( int i = 0; i < scores.length; i++ )
{
total += scores[i];
}
System.out.print( "평균 성적은 : " + total / scores.length + "입니다." );
}
}
배열의 크기를 입력 : 3
성적을 입력하시오 : 1
성적을 입력하시오 : 2
성적을 입력하시오 : 3
평균 성적은 : 2입니다.
배열 전체가 전달되는 배열 참조 변수 복사 예제
package me.basic.step2.primitive;
import java.util.Scanner;
public class ArrayExam4 {
final static int STUDENT = 5;
public static void main(String[] args) {
int[] scores = new int[STUDENT];
getValues( scores );
getAverage( scores );
}
private static void getValues( int[] array ) {
Scanner sc = new Scanner( System.in );
for( int i = 0; i < array.length; i++ )
{
System.out.println( "성적을 입력하시오." );
array[i] = sc.nextInt();
}
}
private static void getAverage( int[] array )
{
int total = 0;
for ( int i = 0; i < array.length; i++ ){
total += array[i];
}
System.out.println( "평균 성적은 " + total / array.length + "이다" );
}
}
성적을 입력하시오.
50
성적을 입력하시오.
40
성적을 입력하시오.
60
성적을 입력하시오.
80
성적을 입력하시오.
70
평균 성적은 60이다
Process finished with exit code 0
객체들의 배열
자동차를 나타내는 Car 클래스가 있다고 가정하고 배열을 작성하면
Car[] cars = new Car[5];
위 예제 와 같이 배열이 만들어지고 참조값을 저장할 수 있는 5개의 공간이 만들어진다. 즉, 실제 객체가 생성되어 저장되는 것은 아니다.
실제 객체는 다음과 같이 생성하여야 한다.
cars[0] = new Cars();
cars[1] = new Cars();
class Car{
public int speed;
public int gear;
public String color;
public Car() {
speed = 0;
gear = 1;
color = "red";
}
public void speedUp() {
speed += 10;
}
public String toString(){
return "속도 : " + speed + "기어 : " + gear + " 색상 " + color;
}
}
public class CarArray {
public static void main(String[] args) {
final int NUM_CARS = 5;
Car[] cars = new Car[NUM_CARS];
for( int i = 0; i < cars.length; i++ )
cars[i] = new Car();
for( int i = 0; i < cars.length; i++ )
cars[i].speedUp();
for( int i = 0; i < cars.length; i++ )
System.out.println( cars[i] );
}
}
위 예제의 값이 어떻게 나올 것 같은가?
얼핏 봤을때는 Car 클래스의 cars 객체 참조 변수를 활용하여 speedUp을 호출했다면 += 값으로 인해 속도가 계속 증가하는것으로 볼 수 있다. 하지만 아니다.
위 예제는 실 원소가 아니라 참조 변수를 가르키고 있으므로 결과는 다음과 같다.
속도 : 10기어 : 1 색상 red
속도 : 10기어 : 1 색상 red
속도 : 10기어 : 1 색상 red
속도 : 10기어 : 1 색상 red
속도 : 10기어 : 1 색상 red
Process finished with exit code 0
즉, Car객체가 배열 우너소에 저장되는 것은 절대 아닌다. 참조 변수는 객체가 아니라 객체의 참조값 을 저장하고 있기 때문에 Car배열에서도 각각의 원소들은 참조값만을 저장하게 된다.
2차원 배열
자료형[][] 참조변수이름 = new 자료형[행크기][열크기]
for( int i = 0; i < 3; i++ )
for( int j = 0; j < 5; j++ )
System.out.println( s[i][j] );
자료형[][] 참조변수이름 = { { 10, 20, 30} , { 40, 50, 60 } }
1차원 배열의 경우 하나의 length필드가 존재했지만 2차원 배열에서는 약간 복잡해진다. 각 행마다 별도의 length필드가 있고 이것은 각 행이 가지고 있는 열의 개수를 나타낸다.
package me.basic.step2.primitive;
public class ArrayExam5 {
public static void main(String[] args) {
int[][] array = { { 10, 20, 30, 40}, { 50, 60, 70, 80 } };
for( int r = 0; r < array.length; r++ )
for ( int c = 0; c < array[r].length; c++ )
System.out.println( r + "행" + c + "열" + array[r][c] );
}
}
0행0열10
0행1열20
0행2열30
0행3열40
1행0열50
1행1열60
1행2열70
1행3열80
Process finished with exit code 0
다차원 배열
자료형[][][] 참조형변수이름 = new double[3][2][12];
타입 추론
타입이 정해지지 않은 변수에 대해서 Compailer가 변수의 타입을 스스로 찾아낼 수 있도록 하는 기능
타입 추론이 가능하다는 얘기는 곧 타입을 명시하지 않아도 된다는 말
이말은 코드량을 좀더 줄이고 코드의 가독성을 높일 수 있다는 뜻
java 10버전부터 사용 가능
자바에서는 일반 변수에 대해 타입 추론을 지원하지 않기 때문에
일반변수에 대해서는 타입추론이 지원되지 않고, generics와 lambda식에 대해서만 타입 추론이 지원되고 있다.
var title = "java10 type";
String title = "java10 type";
제네릭의 연산자(<>)에 타입을 넘겨줄때 타입 정보를 제거한다.
// Integer 타입을 -> Object형태로 변환
List<Integer> age; -> List<Object> age;
자바8 이전에 제네릭 타입의 인자를 넘겨주는 경우 타입 추론이 안되는 경우가 있었습니다.
// Collections.emptyList() 의 메소드 시그니쳐
public static final <T> List<T> emptyList() { ... }
// 이런 메소드가 있다고 하자
static void processNames(List<String> names) {
for (String name : names) {
System.out.println("Hello " + name);
}
}
// 컴파일러는 제네릭 타입이 String 이라고 유추할 수 있음
List<String> names = Collections.emptyList();
processNames(Collections.emptyList()); // error in Java 7
processNames(Collections.emptyList()); // OK in Java 7
Collections.emptyList() 는 제네릭 타입을 알 수 없기 때문에 List 타입으로 결과를 리턴하게 됩니다. 따라서 processNames() 의 인자는 타입이 맞지 않아 컴파일 에러가 납니다. 하지만 자바8에서 이것이 개선되어 타입 증거 없이도 인자의 타입을 유추할 수 있게 되었습니다.
static class List<E> {
static <T> List<T> emptyList() {
return new List<T>();
}
List<E> add(E e) {
// 요소 추가
return this;
}
}
List<String> list = List.emptyList(); // OK
List<String> list = List.emptyList().add(":("); // error
List<String> list = List.<String>emptyList().add(":("); // OK
emptyList() 메소드를 호출하면서 타입이 제거되기 때문에 연쇄적으로 호출되는 부분에서 인자를 알아챌 수가 없습니다. 자바 8에서 수정될 예정이었으나 취소되어 여전히 컴파일러에게 명시적으로 타입을 알려줘야 합니다.
자바는 람다를 지원하기 위해서 타입 추론을 강화해야 했습니다. 그래서 '함수형 인터페이스’가 나왔습니다. 함수형 인터페이스는 하나의 추상 메소드(Single abstract method, 단일 추상 메소드)로 이루어진 인터페이스인데, 여기서 함수의 시그니쳐가 정의되어 있기 때문에 컴파일러가 이 정보를 참고해서 람다에서 생략된 정보들을 추론할 수 있게 됩니다.
함수형 인터페이스는 단 하나의 메소드를 가질 수 있습니다. 컴파일러가 미리 체크할 수 있도록 @FunctionalInterface 어노테이션으로 표시해줄 수 있습니다. 기존 JDK 의 Runnable 이나 Callabe 같은 인터페이스들이 이 어노테이션으로 개선되었습니다. 또한 다른 사용자에게 인터페이스의 의도를 설명해줄 수도 있습니다.
// 컴파일 OK
public interface FunctionalInterfaceExample {
}
// 추상 메소드가 없으므로 컴파일 에러
@FunctionalInterface
public interface FunctionalInterfaceExample {
}
// 추상 메소드가 두 개 이상이면 컴파일 에러
@FunctionalInterface
public interface FunctionalInterfaceExample {
void apply();
void illigal(); // error
}
만약 함수형 인터페이스를 상속하는 경우에도 이러한 특성을 그대로 이어받습니다.
@FunctionalInterface
interface A {
abstract void apply();
}
// 함수형 인터페이스로 동작
interface B extends A {
}
// 명시적으로 오버라이드 표시 가능
interface B extends A {
@Override
abstract void apply();
}
// 하나의 추상메소드 외에 메소드 추가 불가
interface B extends A {
void illegal(); // error
}
// 함수형 인터페이스에서 정의한대로 람다는 인자가 없고 리턴값이 없는 함수로 사용할 수 있다.
public static void main(String... args) {
A a = () -> System.out.println("A");
B b = () -> System.out.println("B");
}
람다는 인자의 타입을 추론할 수 있습니다. 위에서 살펴본 것처럼 함수형 인터페이스가 타입에 대한 정보를 컴파일러에게 제공한 덕분입니다.
@FunctionalInterface
interface Calculation {
Integer apply(Integer x, Integer y);
}
static Integer calculate(Calculation operation, Integer x, Integer y) {
return operation.apply(x, y);
}
// 람다 생성
Calculation addition = (x, y) -> x + y;
Calculation subtraction = (x, y) -> x - y;
// 사용
calculate(addition, 2, 2);
calculate(substraction, 5, calculate(addition, 3, 2));
@FunctionalInterface 에는 하나의 메소드만 작성할 수 있다고 했는데, 여기에는 예외가 있습니다.
예를 들어 Comparator 의 경우 @FunctionalInterface 인데 메소드가 많이 있습니다. 살펴보면 디폴트 메소드, 스태틱 메소드, Object 오버라이드한 메소드가 있고 추상 메소드의 경우는 compare 메소드 하나 뿐입니다
Coding or Gaming ( https://codingisgame.tistory.com/3 )
프로그래머스
TCP School
박철우의 블로그 ( https://parkcheolu.tistory.com/105 )
Power JAVA
https://futurecreator.github.io/2018/07/20/java-lambda-type-inference-functional-interface/