primitive Data
- boolean, char, byte, short, int, long, float, double
- 아주 가벼운 데이터를 말한다.
- 스택메모리에 머물러있다.
Object Data
- 상대적으로 무거운 데이터이다.
- 실제 데이터는 힙메모리에 공유하고 레퍼런스만 스택메모리에 있다.
Wrapper Class
- primitive Data를 ObjectData화 시킨 Class이다.
💡 primitive Data 에서 Wrapper Class로 자동으로 변환되는걸 autoboxing이라 한다.
💡 Wrapper Class에서 primitive Data 자동으로 변환되는걸 unboxing이라 한다.
public class Main {
public static void main(String args[]) {
ArrayList<Integer> intArrayList = new ArrayList<>();
// 오토박싱
intArrayList.add(10);
System.out.println("intArrayList: " + intArrayList);
// 언박싱
int num = intArrayList.get(0);
System.out.println("num: " + num);
}
}
🚫문제점
private static long sum_with_autoboxing() {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; ++i) {
sum += i;
}
return sum;
}
sum += i -> sum += Long.valueOf(i); 로 바꾸게 된다.
매번 새로운 객체 생성
추상 클래스 vs 인터페이스
디폴트 메소드의 도움으로 인터페이스는 추상 클래스(abstract)와 차이가 없어보입니다. 하지만 실제로 다음과 같은 차이가 있습니다.
우선순위( 같은 이름의 메소드를 사용할때 )
map은 스트림 내부의 요소 하나하나에 접근해서 제가 파라미터로 넣어준 함수를 실행한 뒤 최종연산에서 지정한 형식으로 반환해주는 메서드 입니다.
flatMap을 사용하면 중복 구조로 되어있는 리스트를 하나의 스트림처럼 다룰 수 있습니다.
예시)
animal = ["cat","dog"]
->원하는 결과 = [ "c", "a", "t", "d", "o", "g" ]
List<String[]> results = animals.stream().map(animal -> animal.split(""))
.collect(Collectors.toList());
// 결과 = [ [ "c", "a", "t" ] , [ "d", "o", "g" ] ]
List<String> results = animals.stream().map(animal -> animal.split(""))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
// 결과 = [ "c", "a", "t", "d", "o", "g" ]
Arrays::stream은 배열을 스트림으로 변환해주는 메서드 참조 표현
구체 타입 : 자식메서드를 사용할수 있다.
추상 타입 : 다형성을 이용할수 있다.
생성자 주입
https://programforlife.tistory.com/111
개발자가 final 키워드를 사용한 코드 ( 고정되여야 하는 것 ) 를 변경하려할때 컴파일 에러를 통해 변경을 막을수 있다
synchronized 메소드는 synchronized(this) {} 블럭과 같다고 생각하면 되며, 현재 인스턴스에 대해서 락을 획득한다.
만약 2개 이상의 인스턴스가 있다면 각각의 인스턴스에 대해 모니터락이 걸린 것이므로, 두 개의 인스턴스가 각각의 method를 실행하는 것이 가능하다.
해당 메서드 락을 획득하고 나서 메서드 블록을 벗어나면 락을 반납한다.
//둘 클래스는 같은 거임
public class SynchronizedTest {
public synchronized void a() {... }
public synchronized void b() {... }
}
public class SynchronizedTest {
public static void a() {
synchronized(this) {... }
}
public static void b() {
synchronized(this) {... }
}
}
synchronized를 특정 객체에 선언하는 것은 synchronized 블록을 사용하는 것이다. synchronized 메서드는 현재 객체 자체에 락을 거는 것이지만, synchronized 블록은 특정 객체에만 락을 걸어서 최소한의 필요한 영역에만 락을 걸 수 있다. lock 블록의 범위를 줄인 것이기 때문에 동시성을 향상시킬 수 있다.
💡Reflection : class타입 객체 생성 후 힙 영역에 저장
💡사용
💡hash컬랙션비교 과정
1. 해쉬값 비교
2. 해쉬값이 동일하다면 equals 메소드 이용
💡불변객체 : 불변객체는 재할당은 가능하지만, 한번 할당하면 내부 데이터를 변경할 수 없는 객체
public class Animal {
private final Age age;
public Animal(final Age age) {
this.age = age;
}
public Age getAge() {
return age;
}
}
class Age {
private int value;
public Age(final int value) {
this.value = value;
}
public void setValue(final int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Animal 클래스는 final을 사용하고, Setter를 구현하지 않았지만 불변객체가 될 수 없습니다. 왜냐하면 Animal 클래스의 필드인 Age의 값을 아래처럼 변경할 수 있기 때문입니다.
public static void main(String[] args) {
Age age = new Age(1);
Animal animal = new Animal(age);
System.out.println(animal.getAge().getValue());
// Output: 1
animal.getAge().setValue(10);
System.out.println(animal.getAge().getValue());
// Output: 10
}
https://velog.io/@conatuseus/Java-Immutable-Object%EB%B6%88%EB%B3%80%EA%B0%9D%EC%B2%B4
class MySingleton {
private static MySingleton instance;
public static synchronized MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
public class Singleton {
private volatile static Singleton instance;
private Sigleton() {}
// Lazy Initialization. DCL
public Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
https://ttl-blog.tistory.com/89
https://velog.io/@ljo_0920/java-%EB%B2%84%EC%A0%84%EB%B3%84-%EC%B0%A8%EC%9D%B4-%ED%8A%B9%EC%A7%95
💡String
💡StringBuffer
💡StringBuilder
정리
마지막으로 각 클래스별 특징을 정리해 보겠습니다. 컴파일러에서 분석 할때 최적화에 따라 다른 성능이 나올 수도 있지만 일반적인 경우에는 아래와 같은 경우에 맞게 사용하시면 될 것 같네요.
ArrayList의 구현코드를 보면서 add 메서드를 따라가보았다. 참 재밌는 일이 벌어진다.
결론부터 말하자면 ArrayList는 배열이다
new ArrayList(); 가 호출이 되면 빈 Object 배열 객체가 elementData에 할당이 된다. size는 주석의 설명대로 ArrayList의 크기이다.
add가 호출이되면 또 다른 오버로딩된 add를 호출한다. 넣을 element 와 가지고 있는 element 배열 그리고 size를 넘겨준다. 현재 사이즈가 배열의 길이와 같다면 grow 함수를 호출 그렇지 않다면 해당 인덱스에 값 저장 후에 사이즈를 늘려준다.
grow 메서드에서는 size+1을 파라미터로 넘겨준다. 이제 이 값을 minCapacity로 받아 Arrays.java 의 copyOf 메서드를 호출한다. 그 전에 minCapacity를 newCapacity를 거치게 하는데, 여러 조건들이 붙지만 이것도 간단히 살펴보면 이전 elementData의 길이에 해당 길이의 반을 더한 길이를 리턴해준다. 근데 이 값이 minCapacity 보다 작다면 minCapacity 값을 리턴한다(2개의 조건이 더 있지만 코드를 직접 보길 바란다).
이제 copyOf 에서 현재 배열, 새로운 길이, 배열의 클래스 정보를 또 다른 copyOf에 넘긴다.
넘긴 배열이 Object 배열 클래스 정보와 같으면 해당 길이의 Object 배열을 copy 변수에 초기화 그렇지 않다면 새로운 타입의 배열을 저장한다. 이후 arraycopy를 이용해 기존의 배열의 copy에 앞에서부터 복사하고 이 배열을 리턴해주는 형식이다.
//ArrayList.java 내의 코드
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
//Arrays.java 내의 코드
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
@HotSpotIntrinsicCandidate
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
String 클래스는 아래와 같이 모든 문자에 대한 해시 함수를 계산하는 게 아니였다.
skip을 통해 일정 간격의 문자를 건너가면서 해시 함수를 계산했다.
아래 코드를 보면 문자열의 길이가 16을 넘으면 최소 하나의 문자가 건너뛰어지며 해시 함수가 계산된다.
public int hashCode() {
int hash = 0;
int skip = Math.max(1, length() / 8);
for (int i = 0; i < length(): i+= skip)
hash = s[i] + (37 * hash);
return hash;
}
이 방식은 심각한 문제를 얘기한다. 웹상의 URL은 앞부분이 동일한 경우가 많은데 서로 다른 URL에 대해서 동일한 해시 값이 나오는 문제가 생겼기 때문이다. HashSet, HashMap과 같이 HashCode를 사용하여 해시 버킷에 데이터를 저장할 때 중복되는 hashCode는 해시 버킷에 데이터를 LinkedList 형식으로 연결해서 저장하기 때문에 hashCode가 중복될 수록 성능에 영향을 준다.
Java 8에서는 String 클래스의 hashCode 메서드에도 성능 향상을 위해 31을 사용한다. 이 방식은 연산을 빠르게 처리할 수 있다. 31 * N = 32N - N으로서 32는 2^5이니 어떤 수 N에 대해 32를 곱한 값은 시프트연산으로 쉽게 구현할 수 있다. 따라서 N에 31을 곱한 값은 (N << 5) - N이다. 31을 곱하면 이렇게 쉬프트 연산을 통해 빠른 계산이 가능하기 때문에 31을 사용한다.
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
hashCode 자체가 고르게 사용되고 빠른 계산이 가능하더라도
실제 HashMap, HashSet에서의 해시 버킷의 개수 M은 2^a의 형태가 되기 때문에
해시 버킷 index = x.hashCode() % M을 계싼할 때 x.hashCode()의 하위 a개의 비트만 사용하게 된다. 즉, hashCode가 아무리 고르게 분포되도 해시 값을 2의 승수로 나누면 해시 충돌이 발생할 수 있다.
공통점
차이점
interface Hello {
void print();
}
interface World {
void print();
}
public static void action(Hello hello) {
hello.print();
}
public static void action(World world) {
world.print();
}
public void foo() {
// 익명클래스는 다음과 같이 타입이 지정되어 의미가 분명함
action(new Hello() {
public void print() {
System.out.println("Hello");
}
});
// 람다는 Hello 객체인지 World 객체인지 불분명함
action(()-> System.out.println("Hello"));
// 다음과 같이 타입을 지정해 주어야 함
action((Hello) () -> System.out.println("Hello"));
}
Generic Type으로 Primitie Type을 사용할 수 없다.
Generic이 Compile Time 특성이고 Type 제거라는 특성을 통해 Object로 변할 수 있다.
하지만 Primitive Type은 Object를 상속받은 게 아니기 때문에 불가능하다.
대신 Wrapper Class를 활용하여 해결이 가능하다.
// 불가능!!!
List<int> list = new ArrayList<>();
list.add(17);
// Wrapper Class로 대체, 가능!!!
List<Integer> list = new ArrayList<>();
list.add(17);
NIO는 연결 클라이언트 수가 많고 하나의 입출력 처리 작업이 오래걸리지 않는 경우에 사용하는 것이 좋음, 스레드에서 입출력 처리가 오래 걸린다면 대기하는 작업의 수가 늘어나므로 제한된 스레드로
처리하는 것이 불편할 수 있음. 대용량의 데이터 처리의 경우 IO가 좋다. NIO는 버퍼 할당 크기가 문제가 되고, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 즉시 처리하는 IO보다 조금 더 복잡.
연결 클라이언트 수가 적고 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있는 경우
IO로 서버를 구현하는 것이 좋음.
https://velog.io/@alsgus92/JAVA-IO-vs-NIO
💡pure OOP의 조건
1. Encapsulation/Data Hiding 캡슐화/은닉화
2. Inheritance 상속
3. Polymorphism 다형성
4. Abstraction 추상화
5. All predefined types are objects 이미 작성된 타입이 모두 객체
6. All user defined types are objects 사용자가 작성하는 것도 모두 객체
7. All operations performed on objects must be only through methods exposed at the objects.
모든 연산은 반드시 객체 안에 있는 메소드를 통해
이걸 단적으로 잘 지키는 언어는 Smalltalk라고 할 수 있다.
💡왜 Java는 pure OOP 언어가 아닌가?
Java는 위 조건들 중 5번과 7번을 충족시키지 못한다.
pure OOP 언어인 Smalltalk에서는 실제로 기본형 타입들도 object(객체)로 표현해두었다.
예를 들어, static function이나 static variable에는 class에 dot(.)을 붙여 인스턴스 없이 바로 접근할 수 있다. 이런 점이 pure OOP 스럽지 않다고 할 수 있다.
public class BoxingExample {
public static void main(String[] args) {
Integer i = new Integer(10);
Integer j = new Integer(20);
Integer k = new Integer(i.intValue() + j.intValue());
System.out.println("Output: "+ k);
}
}
위 코드의 문제점
Integer 인스턴스를 만들 때 사용되는 10, 20이 Java에게는 int형이다.
덧셈을 할 때도 바로 할 수 없고 .intValue()를 통해 int형으로 변환하여 사용
.효율성을 위해서라고 한다. Primitive Type의 변수는 값을 직접 포함한다. 참조형 타입의 변수는 메모리 내의 다른 곳에 저장된 객체를 참조하는 참조이다.
Wrapper Type의 값을 사용해야 할 때마다 JVM은 객체를 메모리에서 찾아 값을 가져와야 한다. 반면에 값이 포함된 객체에 대한 참조 대신 변수 자체에 값이 포함되어 있는 Primitive Type에는 메모리에 접근할 필요가 없다.
객체인 Wrapper는 힙 영역에 저장된다. Primitive는 단지 "값"이기 때문에 스택 영역에 들어간다. 힙의 래핑된 Primitive의 경우 스택에 있는 값과 Wrapper 객체에 대한 참조가 둘 다 필요하기 때문에 더 효율적이다.
스프링/스프링 부트는 @ComponentScan 을 통하여 빈들을 찾고, 등록함.
basePackages, basePackageClasses로 스캔 시작할 위치를 지정, 생략시 스캔 위치가 현재 클래스의 패키지로 지정.
ComponentScanAnnotationParser가 @ComponentScan를 읽으며 설정 파일 파싱.
ComponentScanAnnotationParser는 ClassPathBeanDefinitionScanner 를 호출하며 현재 base package를 기점으로 하위 패키지들을 탐색, 패키지의 모든 파일들의 메타데이터를 확인 & @Component 어노테이션(및 stereo annotation)이 적용된지 확인.
이후 매개변수로 넘어온 빈 팩토리(registerBeanDefinition, 레지스터)에 빈 등록.