자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.
우리가 주로 사용하는 값의 종류는 크게 문자와 숫자로 나눌 수 있으며 숫자는 다시 정수와 실수로 나눌 수 있다.
기본형( primitive type )은 모두 8가지 타입( 자료형 )이 있으며, 크게 논리형, 문자형, 정수형, 실수형으로 구분된다.
int num = Integer.parseUnmsignedInt("4294967295");
String numString = Integer.toUnsignedString(num);
System.out.println(numString);
BigInteger bigInteger = BigInteger.valueOf(2200000000L);
실수형은 정수형과 저장 방식이 다르기에 같은 크기라도 훨씬 큰 값을 표현할 수는 있지만, 오차가 발생할 수 있다.
그래서 정밀도가 중요한데, 정밀도가 높을 수록 오차의 범위가 줄어든다.
위 표를 보면 float의 정밀도는 7자리로 10진수로 7자리의 수를 오차없이 저장할 수 있다는 의미이다. 그렇기에 사용할 변수의 값의 범위가 7가지를 넘는다면 정밀도를 고려해 double 타입을 사용해야 한다.
float number = 0f;
for(int i = 0; i < 10; i++) {
number += 0.1f;
}
System.out.println(number);
====================
1.0000001
BigDecimal number = BigDecimal.ZERO;
for(int i = 0; i < 10; i++) {
number = number.add(BigDecimal.valueOf(0.1));
}
System.out.println(number);
====================
1.0
자료형은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
추가적으로 기본형은 JVM 메모리의 스택영역에 실제 값들이 저장된다면, 참조형은 실제 인스턴스는 힙 영역에 생성되있고, 그 영역의 주소를 스택영역에서 저장하고 있다고 보면 된다.
int a = 2;
float b = 19.2;
Car c = new Car("kia", 100);
그 자체로 값을 의미하는 것
리터럴은 데이터 그 자체를 의미한다.
원래 12, 123와 같은 값들이 상수이지만 프로그래밍에서 상수를 값을 한번 저장하면 변경할 수 없는 저장공간으로 정의했기 때문에 이와 구분하기 위해서 리터럴이라는 용어를 사용한다.
그러니 리터럴은 단지 우리가 기존에 알고 있던 상수의 다른 이름일 뿐이다.
인스턴스안의 값의 불변셩이 보장된다면 객체 리터럴이 될 수 있다. ( 불변 클래스 )
하지만 이렇게 불변성을 보장하도록 설계된 클래스를 제외하고 보통의 인스턴스는 동적으로 사용되고 내용이 변할 수 있기 때문에 객체 리터럴이 될 수 없다.
10진수 이외에도 2, 8, 16진수로 표현된 리터럴을 변수에 저장할 수 있다.
16진수는 접두사 0x(X), 8진수는 접두사 0을 붙인다.
int octNum = 010; // 8진수 10, 10진수로 8
int hexNum = 0x10; // 16진수 10, 10진수로 16
int binNum = 0b10; // 2진수 10, 10진수로 2
JAVA 7부터는 정수형 리터럴의 중간에 구분자’_’를 넣을 수 있다.
long big = 100_000_000_000;
double a = 0.1; // 0.1
double b = 1e-1; // 0.1
float c = 0.1f; // 0.1
char a = 'a';
출처) https://mine-it-record.tistory.com/100
String a = "abc";
boolean a = true;
boolean b = false;
변수를 사용하려면 먼저 변수를 선언해야 한다.
int week;
int -> 변수 타입
week -> 변수 이름
타입(type)
인지 지정하는 것저장할 수 있는 메모리 공간
을 의미하므로 변수 이름은 이 메모리 공간에 이름을 붙혀주는 것변수를 선언하면, 메모리의 빈 공간에 변수타입
에 알맞는 크기의 저장공간이 확보되고, 앞으로 이 저장공간은 변수 이름
을 통해 사용할 수 있게 된다.
변수를 사용하기 전에 처음으로 값을 저장하는 것
변수를 선언하면 메모리에 변수의 저장공간이 확보되어 있지만, 이 공간 안에 어떠한 값이 저장되어 있을 지는 알 수 없다. 메모리의 경우 여러 프로그램이 공유하는 자원이므로 전에 다른 프로그램에 의해 저장된 알 수 없는 값(쓰레기 값)
이 남아 있을 수 있기 때문이다.
그렇기에 초기화를 해줘야 한다.
int week = 7;
변수에 값을 저장할 때는 대입 연산자 =
를 사용한다.
대입 연산자의 우측의 값을 좌측에 있는 변수에 저장하라는 뜻이다.
지역변수는 사용되기 전에 초기화를 반드시 해야 한다.
public class Test {
public static void main(String[] args) {
int a = 3;
}
}
iconst_3
은 정수 3을 호출 스택에 올린다는 뜻이고 istore_1
은 정수를 첫 번째 변수에 저장한다는 뜻이다. ( 여기서는 a )
이 말은 코드가 한줄로 되어있더라도 내부적으로는 두 번으로 나눠서 일을 처리한다는 것이다.
그렇기 때문에 멀티 쓰레드 환경에서 여러 가지 Race Condition문제가 발생한다.
public class Test {
public static void main(String[] args) {
BigDecimal number = BigDecimal.ZERO;
number = number.add(BigDecimal.valueOf(0.1));
}
}
이러한 코드가 있다고 가정해보자. ( 이해를 돕기 위한 코드이다. )
만약 A라는 쓰레드가 add라는 메서드를 처리하고 다음 할당을 해야하지만 이때 B라는 쓰레드가 add라는 메서드를 처리한 뒤 A쓰레드가 할당하게 된다면 값이 이상해지게 된다.
그러므로 변수 선언과 할당을 한번에 했다해서 한줄로 실행되는 것이 두줄로 실행되는 것이다.
지역변수는 변수의 초기화로 충분하지만, 멤버변수의 초기화는 몇가지 방법이 더 있다.
명시적 초기화
초기화 블럭
클래스 초기화
블럭과 인스턴스 초기화
블럭으로 나뉜다.public class Test {
static{
// 클래스 초기화 영역
}
{
// 인스턴스 초기화 영역
}
}
생성자
변수는 클래스변수, 인스턴스변수, 지역변수 모두 세 종류가 있다. 변수의 종류를 결정짓는 중요한 요소는 변수의 선언된 위치
이므로 변수의 종류를 파악하기 위해서는 변수가 어느 영역에 선언되었는지를 확인하는 것이 중요하다.
public class Test {
int instanceValue; // 인스턴스 변수
static int classValue; // 클래스변수( static 변수, 공유변수 )
void method(){
int localValue; // 지역변수
}
}
클래스 내부에 선언되는 변수를 멤버변수라고 한다. 여기서 키워드static
과 함계 선언되는 변수를 클래스 변수, 붙지 않은 것을 인스턴스 변수라고 한다. 그리고 멤버변수를 제외한 나머지 변수들은 모두 지역변수이다.
인스턴스 변수 (instance variable)
먼저 인스턴스를 생성해야 한다.
public class Car {
int position;
public Car(int position) {
this.position = position;
}
}
Car car1 = new Car(1);
Car car2 = new Car(2);
클래스 변수 (class variable)
static
을 붙일 경우 클래스 변수가 되며 한 클래스의 모든 인스턴스가 값을 공유한다. (공통된 저장 공간)public class Car {
static int position = 3;
...
}
public static void main(String[] args) {
System.out.println(Car.position);
}
=============================
3
지역 변수 (local variable)
public static void main(String[] args) {
for (int i = 0; i < 10; i++){
System.out.println(i);
}
// i가 지역 변수!
}
프로그램 실행 도중 클래스에 대한 정보가 요구될 때, 클래스는 메모리에 로딩된다.
( 해당 클래스가 이미 메모리에 로딩되어 있다면, 또 다시 로딩하지 읺는다. )
public class Test {
static int classValue = 1;
int instanceValue = 1;
static{
classValue = 2;
}
{
instanceValue = 2;
}
public Test(){
instanceValue = 3;
}
}
클래스 변수는 항상 인스턴스 변수보다 먼저 생성 및 초기화된다.
변수 또는 상수의 타입을 다른 타입으로 변환하는 것
프로그램을 작성하다 보면 같은 타입뿐만 아니라 서로 다른 타입간의 연산을 수행햐야하는 경우도 있다. 이럴 때는 연산을 수행하기 전에 타입을 일치시켜야 하는데, 변수나 리터럴의 타입을 다른 타입으로 변환해주는 것을 형 변환
이라고 한다.
(타입)피연산자
변환할 변수나 리터럴 앞에 타입을 괄호와 함께 붙여주기만 하면 된다. 이 때 형 변환 연산자는 그저 피연산자의 값을 읽어서 지정된 타입으로 형변환하고 그 결과를 반환할 뿐이기에 기존의 변수나 리터럴이 변화되지는 않는다.
double d = 810.4;
int score = (int)d // 810
기본형 변수 중 boolean을 제외한 나머지 타입들은 서로 형변환이 가능하다.하지만 타입간에 각 가지고 있는 크기가 다르기 때문에 형변환을 통해 크기의 차이만큼 값이 잘려나감으로써 값 손실(loss of data)
이 발생할 수 있다.
경우에 따라 형변환을 생략할 수 있다. 컴파일러가 생략된 형변환을 자동적으로 추가하여 생략할 수 있게 되었다.
하지만 변수가 저장할 수 있는 크기보다 더 큰 값을 저장하려는 경우에 형변환을 생략하면 에러가 발생한다. 이는 더 작은 값으로 할당되며 값 손실이 발생할 수 있기 때문에 이를 명시적 형변환으로 바꾸어 주면 에러가 발생하지 않는다.
byte b = 10000; // 에러 발생, byte의 범위를 초과한다.
byte c = (byte)10000; //명시적 형 변환으로 에러가 발생하지 않는다.
기존의 값을 최대한 보존할 수 있는 타입으로 자동 형변환한다.
표현범위가 좁은 타입에서 넓은 타입으로 형변환하는 경우에는 값 손실이 없으므로 두 타입 중에서 표현범위가 더 넓은 쪽으로 형변환된다.
추가적으로 몇가지 규칙이 더 있다.
long a = 20L;
double b = 10.0;
// a + b -> double형
1번 규칙에 대해 더 살펴보자면, 자바 바이트코드 opcode에는 byte와 short를 스택 메모리에 적재하는 명령어가 없기 때문에 byte와 short는 무조건 int로 메모리에 올라가게 된다.
이 말은 아래 예제를 통해 알 수 있다.
public class Test {
public static void main(String[] args) {
byte a = 10;
short b = 100;
int c = a + b;
}
}
바이트코드 중 0번과 3번 라인의 bipush
명령어는 byte, short 자료형을 int 자료형으로 스택에 푸시하는 명령어 이다. 즉, 스택에 애초에 int로 올라가게 된다는 것이다.
그래서 아래와 같은 상황이 발생한다.
public class Test {
public static void main(String[] args) {
byte a = 10;
byte b = 20;
byte c = a + b; //error java: incompatible types: possible lossy conversion from int to byte
}
}
컴파일 에러가 발생하게 된다. 발생하지 않게 하면 변수 c의 자료형을 int로 바꾸거나, a+b를 byte로 캐스팅 해주어야 한다.
int c = a+b;
byte c = (byte)a+b;
Java에서는 primitive
타입에 대한 Wrapper
클래스가 있다. Java 5이후로는 이러한 값 끼리 명시적인 형변환을 해줄 필요가 없는데, Java 컴파일러가 이를 대신 해주기 때문이다.
오토박싱과 언박싱을 사용하면 개발자가 더 깔끔한 코드를 작성할 수 있어 가독성이 높아지게 된다.
오토박싱은 primitive
타입에서 Wrapper
클래스로 자동 변환되는 것이다.
int → Integer로 변환하는 식으로 말이다.
Integer value = 1;
이 경우 컴파일러가 Interger value = Integer.valueOf(1);
로 변환한다.
언박싱은 오토박싱과 반대로 Wrapper
타입을 primitive
타입으로 변환해준다. Java에서는 Wrapper
클래스의 객체가 다음과 같은 경우 일대 언박싱을 해준다.
primitive
타입으로 매개변수를 받을 때, 매개변수로 전달되는 경우primitive
타입의 변수에 할당되는 경우public class Test {
public static void main(String[] args) {
Integer value = 1;
printValue(value); // 매개변수로 전달되는 경우
int i = value; // primitive 타입의 변수에 할당되는 경우
}
public static void printValue(int value){
System.out.println(value);
}
}
primitive
타입의 형변환은 값 자체의 변환을 의미하지만 객체의 형변환은 참조 변수에서 객체를 바라보는 관점의 변환을 의미한다. 즉, 힙에 있는 객체 자체는 변경 되지 않다는 것이다.
자식 클래스에서 부모 클래스로 캐스팅하는 것을 업 캐스팅
이라고 하고, 컴파일러에 의해 수행된다.
업 캐스팅은 상속과 밀접한 관련이 있다.
public class Animal {
public void eat(){
//...
}
}
public class Cat extends Animal{
@Override
public void eat() {
//...
}
public void meow(){
//...
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
//새로 생성된 Cat 객체는 Animal 타입의 참조 변수에도 할당할 수 있다.
Animal animal = cat;
//위는 다음과 같이 컴파일러에 의해 변경된다.
//Animal animal = (Animal)cat;
}
}
참조변수는 선언된 타입의 하위 타입을 참조할 수 잇다. 대신, 그 타ㅣㅂ에서 사용할 수 있는 메서드는 제한될 수 있다. 하지만 인스턴스 자체는 변경되지 않는다.
cat에서는 meow()
를 호출할 수 있지만, animal에서는 meow()
를 호출할 수 없다.
업 캐스팅 덕분에 우리는 다형성을 활용할 수 있다.
가끔 우리는 위와 같이 상위 타입으로 참조하는 하위 타입 객체를 활용하는 경우가 잇다. 이 때, 하위 타입에서만 제공하는 메서드를 사용하고 싶은 경우 우리는 다운 캐스팅
을 활용한다.
다운 캐스팅은 부모 클래스에서 자식 클래스로 명시적 캐스팅
을 하는 것이다.
((Cat) aniaml).meow();
위 코드는 명시적으로 Cat 클래스로 캐스팅했으며, 정상적으로 동작한다. 하지만 Animal클래스를 상속하는 다른 클래스의 인스턴스를 사용한다거나, Animal 클래스의 인스턴스를 사용한 경우 ClassCastException
이 발생하게 된다.
문제없이 컴파일되지만, 런타임에 예외가 발생하게 된다.
다만, 관계가 없는 타입으로 캐스팅하는 경우엔 컴파일 에러가 발생한다. 즉, 서로 연관이 있어야만 컴파일이 되고, 연관이 있더라도 타입이 일치하지 않는다면, 런타임에 ClassCastException
이 발생하게 된다.
String s = (String) animal
우리는 이런 런타임 예외를 방지하기 위해 instanceof
연산자를 사용할 수 있다.
if (animal instanceof Cat) {
((Cat) animal).meow();
}
배열의 선언
int[] score; // 타입[] 변수이름;
int score[]; // 타입 변수이름[];
배열의 생성
타입[] 변수이름 = new 타입[길이];
new
를 사용해 배열의 타입과 길이를 지정하면 메모리에 해당 길이만큼 영역을 확보한다.int[] score = new int[5];
score[0] = 1;
....
or
int[] score = new int[]{1, 2, 3, 4, 5};
or
int[] score = {1, 2, 3, 4, 5};
score은 Runtime Stack
영역의 Heap
영역 주소값을 가짐
Heap
영역에 int 타입 크기의 요소 5개를 할당하여 사용함.
2차원 배열
int[][] score = new int[2][2];
score[0][0] = 1;
....
or
int[][] score = new int[][]{{1,2},{3,4}};
or
int[][] score = {{1,2},{3,4}};
Runtime Stack
영역의 score는 2개의 요소 크기(2개 요소에 주소값을 가지고 있음)를 가진 Heap
영역 주소값을 가짐Heap
영역에는 실제 값이 들어있는 요소들과 주소값이 들어있는 요소들로 존재함.출처) https://catsbi.oopy.io/6541026f-1e19-4117-8fef-aea145e4fc1b
자바 컴파일러에서 타입을 추론하는 것을 Type Inference
라고 한다.
이 타입추론을 하기 위해 메서드 호출과 호출할 대 사용하는 인수을 결정하기 위한 선언부터를 살펴본다. 추론 알고리즘 (inference algorithm)
을 통해 인수 타입을 결정하고 가능하다면 결과가 할당되는 타입(인수타입)이나 반환 타입도 추론한다.
public class Test {
static <T> T pick (T a1, T a2) {
return a2;
}
public static void main(String[] args) {
Serializable d = pick("d", new ArrayList<String>());
}
}
pick
메소드의 매개변수는 T
이고 메소드의 매개변수 a1,a2 둘다 T
타입이다.하지만 pick
메서드를 호출하면서 첫 번째 인자로 String
을 주고 두 번째 인자로 ArrayList
를 주었다. 이런 경우 모든 인자에 어울리는 선(공통 부모)란 Serializable
으로 String
과 ArrayList
둘다 Serializable
을 구현하고 있기 때문이다.
https://devlog-wjdrbs96.tistory.com/268
타입추론덕분에 generic
메서드를 사용할 때 보통의 메서드처럼 특정 타입을 명시하지 않은 채로 호출할 수 있다.
public class BoxDemo {
public static <U> void addBox(U u, List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
List<Box<Integer>> listOfIntegerBoxes = new ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); //---(1)
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);//---(2)
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
(1) : addBox라는 generic 메서드를 호출할 때 <Integer> type witness
와 함께 type parameter
를 명시하여 사용할 수 있다.
(2) : Java 컴파일러가 메서드의 인자로부터 자동으로 Integer type
임을 추론해주기 때문에 type witness
를 생략할 수 도 있다.
Java컴파일러가 컨텍스트로부터 타입추론이 가능하다면 Generic 클래스의 생성자를 호출하기 위해 필요한 type arguments
를 비어있는 type witness(<>)
로 대체할 수 있다.
List<String> list1 = new ArrayList<String>();
List<String> list2 = new ArrayList<>();
클래스가 Generic, non-generic 인지 그 여부와 관계없이 생성자는 generic일 수 있다.
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
public static void main(String[] args) {
MyClass<Integer> myInstance = new MyClass<Integer>("");
}
X
에는 Integer
가 들어가지만 생성자의 type 매개변수 T
에는 String
이 들어간다.Java7 이전에는 컴파일러에서 실제 type argument
를 작성해 타입 추론을 할 수 있었지만, 이후로 컴파일러는 the diamond(<>)
를 사용하는 경우 다음과 같이 generic 클래스의 실제 type argument
까지 추론이 가능하다.
MyClass<Integer> myInstance = new MyClass<>("");
Java컴파일러는 generic method invocation
의 type argument
를 추론하기 위해 target typing
의 이점을 이용한다. 표현식의 target type
이란 표현식이 나타낸 위치에 의존하여(컨텍스트 의존) Java 컴파일러가 기대하는 데이터 타입이다.
static <T> List<T> emptyList() { return new ArrayList<>(); }
List<String> listOne = Collections.emptyList();
위 코드는 Collection API
의 emptyList
함수를 이용해 List<String>
객체를 반환한다. 이런 데이터 타입을 Target Type
이라 하는데 emptyList
함수가 List<T>
타입을 리턴하기에 컴파일러에서 type argument T
가 반드시 String
일 것이라고 추론한다.
물론, type witness
를 사용해 명시적 선언을 해줄 수도 있지만 위 코드에서는 불필요하다.
List<String> listOne = Collections.<String>emptyList(); //불필요한 witness
하지만 type witness
가 필요한 경우도 있다.
void processStringList(List<String> stringList) {
//process stringList
}
public static void main(String[] args) {
processStringList(Colections.emptyList());
}
Java 7컴파일러에서는 컴파일 되지 않고 에러가 발생하며 에러 메시지가 출력된다.
List<Object> cannot be converted to List<String>
이런 에러가 발생하는 이유는 컴파일러는 type argument T
를 위한 value를 필요한데, 아무것도 주어지지 않았기에 Object
를 value로 삼게된다. 그 결과 Collections.emptyList()
는 List<Object>
객체를 리턴하며 이는 processStringList
에서 호환하지않는 인수타입이기에 에러가 발생한다.
그렇기에 Java7에서는 type witness
를 명시해줘야 한다.
processStringList(Colections.<String>emptyList());
하지만 Java 8부터는 위와 같은 경우에 type witness
를 명시해주지 않아도 Tartget type
을 결정할 때 메서드의 argument도 살피도록 확장되었기 때문에 에러가 발생하지 않는다.
그렇기 때문에 Java 8이상에서는 위의 type witness
가 없는 메서드 호출도 정상적으로 동작한다.
Java 10부터 추가된 특징 중 하나인 Local Variable Type Inference
는 로컬 변수 선언을 var를 이용하여 기존의 엄격한 타입 선언방식에서 컴파일러에게 타입추론 책임을 위임할 수 있게 되었다.
var list = new ArrayList<String>(); //infers ArrayList<String>
var stream = list.stream(); //infers Stream<String>
var numbers = Arrays.asList(1, 2, 3, 4, 5);
for (var i = 0; i < numbers.size(); i++) {
System.out.println("numbers = " + numbers.get(i));
}
var numbers = Arrays.asList(1, 2, 3, 4, 5);
for (var number : numbers) {
System.out.println(number);
}
IntBinaryOperator plus10 = (@NonNull var one, @NonNull var two) -> one + two + 10;
비어있는 type witness를 사용하면 Object로 추론한다.
하지만 이러한 타입 추론, var이 만능은 아니다. 특히, Java에서는 객체지향 패러다임을 사용하기 때문에 문제가 될 수 있다.
예를 들어, Animal
클래스가 있고, 이 클래스의 자식 클래스인 Cat
, Dog
가 있다고 가정해보자.
Var v = new Cat();
v
의 경우 어떻게 추론이 될까? Aniaml? Cat? 이 경우 컴파일러는 초기화한 클래스의 타입을 사용하게 된다.
따라서 다음의 코드는 컴파일되지 않는다.
v = new Dog();
즉, 다형성을 활용하는 코드는 var타입 추론과 어울리지 않는다는 것이다.
public long add(var list){...} // 불가능
var x; // 불가능
var x = null; // 불가능
var list = new ArrayList<>();
해당 코드의 경우 컴파일이 되지만, 실제 list의 타입은 ArrayList<Object>
로 컴파일되며, generic의 이점을 얻지 못하기 때문에 피하는 것이 좋다.
좋은 글 잘 읽었습니다. 감사합니다
중간에 실수로 float와 double의 범위와 정밀도 표에서 float과 double을 반대로 적으신 것 같습니다.