-
[lg-eureka] 부트 캠프 4일차
intro : 부트캠프 4일차 내용 정리 및 기록.
수업 내용 기록
static 키워드
static 변수는 클래스가 로딩시에 초기화 되며, 클래스의 모든 객체가 공유한다. 인스턴스 변수와는 달리 객체에 속하지 않고 클래스에 속하는 변수이다. 클래스가 로드될 때 초기화되고, 프로그램 종료시까지 메모리에 유지된다.
static 변수예제
class Example {
static int count = 0; // static 변수
public Example() {
count++; // 모든 객체가 공유
}
}
public class Main {
public static void main(String[] args) {
Example obj1 = new Example();
Example obj2 = new Example();
Example obj3 = new Example();
System.out.println("Count: " + Example.count); // 출력: Count: 3
}
}
static 메서드
클래스 레벨에서 호출되는 메서드로 인스턴스화 없이 접근 가능하다. 인스턴스 변수나 메서드를 직접 접근 가능하다.
class MathUtil {
static int add(int a, int b) {
return a + b; // static 메서드는 인스턴스 없이 호출 가능
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MathUtil.add(5, 10)); // 출력: 15
}
}
final 키워드
static final 변수 (상수)
final 변수는 한번 초기화되면 값을 변경할 수 없는 상수가 된다. 특징으로는 변수선언시 즉시 초기화 하거나, 생성자에서 초기화 가능하다. 또한 한번 할당된 값은 다른 값으로 변경할 수 없다. 보통 대문자 이름과 언더스코어로 작성된다.
public class FinalVariableExample {
public static void main(String[] args) {
final int constantValue = 10; // 초기화
System.out.println("Constant: " + constantValue);
// constantValue = 20; // 오류: final 변수는 값을 변경할 수 없음
}
}
static final 메서드
가장 큰 특징으로는 서브클래스에서 재정의 (오버라이딩)이 불가하다, 이렇게 메서드에 final을 사용하는 이유는 메서드의 구현을 고정해서 상속받는 클래스에서 변경하지 못하도록 보호에 목적이 있다.
class Parent {
public final void display() {
System.out.println("This is a final method.");
}
}
class Child extends Parent {
// @Override
// public void display() { // 오류: final 메서드는 오버라이딩 불가
// System.out.println("Cannot override final method.");
// }
}
접근 제한자
public : 어디에서든 접근 가능.
protected : 같은 패키지 + 하위 클래스에서만 접근 가능 (상속관계)
default : 같은 패키지에서만 접근 가능
private : 클래스 내부에서만 접근 가능
(Java의 접근 제한자)
가장 헷갈리는 protected와 default 에제로 이해해보기
protected 키워드 예제 (상속관계가 포인트)
// package1/Parent.java
package package1;
public class Parent {
protected void display() {
System.out.println("Protected method");
}
}
// 같은 패키지
package package1;
public class SamePackage {
public void test() {
Parent parent = new Parent();
parent.display(); // 같은 패키지에서 접근 가능
}
}
// 다른 패키지 - 상속 관계
package package2;
import package1.Parent;
public class Child extends Parent {
public void test() {
display(); // 하위 클래스에서 접근 가능
}
}
// 다른 패키지 - 상속 관계가 아님
package package2;
import package1.Parent;
public class NonChild {
public void test() {
Parent parent = new Parent();
// parent.display(); // 오류: 상속 관계가 아니므로 접근 불가
}
}
default 키워드 예제 (같은 패키지가 포인트)
// package1/Example.java
package package1;
class Example { // default 접근 제한자
void display() {
System.out.println("Default method");
}
}
// 같은 패키지
package package1;
public class Main {
public static void main(String[] args) {
Example example = new Example();
example.display(); // 같은 패키지에서 접근 가능
}
}
// 다른 패키지
package package2;
import package1.Example;
public class Main {
public static void main(String[] args) {
// Example example = new Example(); // 오류: 다른 패키지에서는 접근 불가
}
}
상속
상속은 Java의 객체지향프로그래밍에서 핵심 개념 중 하나로, 기존 클래스의 멤버변수와 메서드를 자식 클래스가 물려받아 사용하는 것을 의미한다. 상속은 코드 재사용성을 높이고 계층적인 구조를 통해 코드의 논리적 설계를 용이하게 만든다. 다만 Java에서는 단일 상속만을 허용하고, 결합도가 높아지는 단점이 존재한다.
아래 코드를 보면, Child는 Parent 클래스를 상속받는다. Child 클래스는 diplay 메서드가 존재하지 않지만, 상속을 통해 상위 클래스의 메서드를 물려받아 실행 할 수 있다.
class Parent {
int value = 10;
void display() {
System.out.println("Parent value: " + value);
}
}
class Child extends Parent {
void show() {
System.out.println("Child value: " + value);
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.display(); // 부모 클래스의 메서드 호출
child.show(); // 자식 클래스의 메서드 호출
}
}
추가적으로 상속에서는 상위 클래스에 기본생성자가 아닌, 매개변수가 있는 생성자가 존재하는 경우 해당 클래스를 상속받는 클래스에서 상위 클래스의 생성자를 호출해주어야 한다. 해당 내용과 관련된 예제 코드는 다음과 같다.
class Parent {
int value;
// 매개변수가 있는 생성자
Parent(int value) {
this.value = value;
System.out.println("Parent Constructor called with value: " + value);
}
}
class Child extends Parent {
int childValue;
// 자식 클래스 생성자
Child(int value, int childValue) {
super(value); // 명시적으로 부모 클래스의 생성자를 호출
this.childValue = childValue;
System.out.println("Child Constructor called with childValue: " + childValue);
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child(10, 20);
}
}
상속에 대한 개념을 알아가면서 오버라이딩(overriding) 개념이 더 부각되게 된다. 부모 클래스의 메서드를 자식 클래스에서 재정의하여 다른 동작을 수행하도록 하는 것을 말한다. 다만 부모 메서드와 동일한 이름, 반환타입, 매개변수를 가져야 한다.
다음 예시는 부모 클래스의 메서드를 자식 클래스에서 재정의 하는 예시이다.
class Parent {
void display() {
System.out.println("This is the parent class method.");
}
}
class Child extends Parent {
@Override
void display() {
System.out.println("This is the overridden method in the child class.");
}
}
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
parent.display();
Child child = new Child();
child.display();
Parent polymorphic = new Child();
polymorphic.display(); // 다형성
}
}
// 출력문 결과
This is the parent class method.
This is the overridden method in the child class.
This is the overridden method in the child class.
위 코드에서 실행결과는 결과는 재정의된 메서드가 실행됨에 따라 Child가 인스턴스화된 객체는
This is the overridden method in the child class. 가 출력된다.
-
[lg-eureka] 부트 캠프 3일차
intro : 부트캠프 3일차 내용 정리 및 기록.
수업 내용 기록
데이터 타입 [기본 타입]
기본형 타입인 byte, short, int, long, float, double, char, boolean 등의 데이터 타입을 말한다. 기본 타입으로 선언된 변수는 값 자체를 저장하고 있지만, 참조 타입으로 선언된 변수는 객체가 생성된 번지 즉 주소값을 보유하고 있다.
데이터 타입 [참조 타입]
객체의 번지를 참조하는 타입, 배열, 열거, 클래스, 인터페이스 같은 것들을 말한다. 기본 타입으로 선언된 변수는 값 자체를 저장하지만, 참조 타입으로 선언된 변수는 객체가 생성된 메모리 번지를 저장한다.
JVM의 힙, 스택, 메소드 영역
JVM은 운영체제에서 할당받은 메모리 영역을 힙 영역, 스택 영역, 메소드 영역으로 구분해서 사용, 벤더사마다 JVM 구조는 디테일 하게는 다르지만 큰 관점에서 공통된 부분은 존재함.
(JVM 예시 이미지)
힙 영역
객체가 생성되는 영역, 객체의 번지는 메소드 영역과 스택 영역의 상수와 변수에서 참조, 결론적으로 객체와 배열을 저장하는 공간.
스택 영역
메소드를 호출할때마다 생성되는 프레임이 저장되는 영역, 메서드 호출과 관련된 로컬 변수 및 참조 변수를 저장.
메소드 영역
클래스 영역이라고도 부르기도한다, 클래스의 메타데이터, 정적 변수(static 변수), 상수(Constant Pool), 그리고 메서드 코드(바이트코드)가 저장됨.
String 타입
String Constant Pool: 리터럴 문자열(“example”)은 JVM의 String Constant Pool에 저장되어 메모리를 절약. 동일한 리터럴이 있을 경우 Pool의 객체를 재사용.
String s1 = "Hello";
String s2 = "Hello"; // s1과 s2는 같은 객체를 참조
new String()을 사용하면 새로운 String 객체가 Heap에 생성된다.
String s1 = new String("Hello"); // Heap에 새 객체 생성
값 목록으로 배열 생성
다음과 같이 값 목록으로 배열을 생성할 수 있다.
int[] arr = {1, 2, 3, 4, 5};
int[] arr = new int[] {1, 2, 3, 4, 5};
다만 다음과 같은 방법으로 배열을 생성하는게 일반적이다.
int [] arr = new int[4];
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
arr[3] = 3;
객체의 개념과 관계
객체란 물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별 가능한 것, 객체는 속성과 동작으로 구성. 자바는 이러한 속성과 동작을 각각 필드와 메소드라고도 말함.
집합관계 : 완성품과 부품의 관계
사용관계 : 다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계
상속관계 : 부모와 자식 관계, 필드 메소드를 물려받음
객체지향 프로그래밍의 특징
[캡슐화]
객체의 데이터(필드)와 행동(메서드)를 하나로 묶고, 외부에서 객체의 내부 구현 세부사항에 접근하지 못하도록 하는 것.
[상속]
부모 클래스(슈퍼 클래스)의 필드와 메서드를 자식 클래스(서브 클래스)가 물려받아 사용하는 것.
[다형성]
같은 형태의 코드(메서드 호출)가 다양한 실행 결과를 만들어내는 것.
[추상화]
복잡한 현실 세계의 개념을 필요한 속성과 행동만 포함하여 프로그래밍적으로 표현하는 것.
클래스와 인스턴스
객체 지향 프로그래밍에서도 객체를 생성하려면 설계도에 해당하는 클래스가 필요함. 클래스로부터 생성된 객체를 해당 클래스의 인스턴스라고 부르며, 클래스로부터 객체를 만드는 과정을 인스턴스화 라고 함, 또한 동일한 클래스로부터 여러개의 인스턴스를 만들 수 있음
this() this의 차이
this()는 생성자에서 다른 생성자를 호출할 때 사용. 동일한 클래스 내에 있는 다른 생성자를 호출하여 코드 중복을 줄이고 객체 초기화를 간결하게 만든다. 주의할 점으로 생성자의 첫 번째 줄에서만 호출할 수 있고, 생성자 체이닝에서 최종적으로는 자기 자신의 초기화가 이루어져야 한다.
this는 현재 객체 자신을 참조하는 키워드. 같은 클래스 내부에서 필드 메서드 생성자 등을 명시적으로 호출하거나 외부에서 전달된 변수와 구분 할때 사용한다.
4조 workshop 토의 내용 기록
메서드 시그니처
메서드 시그니처란 메서드를 고유하게 식별하는 요소를 말한다. 주로 메서드 이름과 매개변수 리스트로 구성되며, 컴파일러가 오버로딩과 오버라이딩을 구분할때 사용한다. 메서드 시그니처의 구성 요소로는 메서드 이름과, 매개변수 리스트(매개변수의 타입, 순서, 개수)를 포함한다. 주의할 점으로 반환타입은 메서드 시그니처에 포함되지 않는다.
객체지향과 절차지향의 차이
[OOP]
현실세계의 객체를 모델링하여 데이터를 객체로 묶어 처리한다. 코드 재사용성이 높고 유지보수성 및 확장성에 장점이 있다.
[POP]
작업을 순차적으로 처리하며 함수 중심으로 설계한다. 특징으로는 데이터와 함수를 분리하는데 단순하여 빠르게 구현 가능함에 장점이 있다.
중요한 둘의 차이점으로는 객체지향은 객체 중심, 절차 지향은 함수 중심에 있다.
String은 언제 heap과 String Constant Pool에 저장될까?
기본적으로 리터럴을 사용한 문자열 String은 String Constant Pool에 저장된다. 만약 new 연산자를 통해 String 객체를 생성하는 경우는 heap 영역에 생성되게 된다. 위 개념은 아래 코드에서 비교연산자 == 를 통해 주소값을 비교할때 true false 값의 결과가 달라진다.
public class Main {
public static void main(String[] args) {
String str = "hello"; // String constant pool에 저장
String str2 = "hello"; // String constant pool에서 재사용
String str3 = new String("hello"); // 별도의 Heap 메모리에 저장
System.out.println(str == str2); // true (같은 객체를 재사용하기 때문에)
System.out.println(str == str3); // false
System.out.println(str.equals(str3)); // true
}
}
위 과정에서 str과 str2는 리터럴값이 같은 문자열이기에 내부적으로 같은 주소값(String Constant Pool)을 할당하게 된다. 그렇기에 str과 str2은 true 값을 반환하며, new 연산자로 생성한 str3는 heap 영역에 할당되게 되어, 새로운 주소값이 할당된 값을 보유하고 있기에 str과 str3은 false 값을 반환하게 된다.
(Heap과 String Constant Pool 영역)
-
[lg-eureka] 부트 캠프 2일차
intro : 부트캠프 2일차 내용 정리 및 기록.
수업 내용 기록
오버 플로우, 언더플로우
// 정수
public class Test {
public static void main(String[] args) {
int a = 128;
byte b = (byte) a;
System.out.println("b = " + b); // -128 (오버 플로우)
a = -129;
b = (byte) a;
System.out.println("b = " + b); // 127 (언더 플로우)
}
}
// 실수
public class Test {
public static void main(String[] args) {
// 오버플로우 예제
float floatMax = Float.MAX_VALUE; // 3.4028235E38
System.out.println("Float MAX: " + floatMax);
float floatOverflow = floatMax * 2; // 오버플로우
System.out.println("Float Overflow: " + floatOverflow); // Infinity
double doubleMax = Double.MAX_VALUE; // 1.7976931348623157E308
System.out.println("Double MAX: " + doubleMax);
double doubleOverflow = doubleMax * 2; // 오버플로우
System.out.println("Double Overflow: " + doubleOverflow); // Infinity
// 언더플로우 예제
float floatMin = Float.MIN_VALUE; // 1.4E-45 (가장 작은 양수)
System.out.println("Float MIN (Positive): " + floatMin);
float floatUnderflow = floatMin / 2; // 언더플로우
System.out.println("Float Underflow: " + floatUnderflow); // 0.0
double doubleMin = Double.MIN_VALUE; // 4.9E-324 (가장 작은 양수)
System.out.println("Double MIN (Positive): " + doubleMin);
double doubleUnderflow = doubleMin / 2; // 언더플로우
System.out.println("Double Underflow: " + doubleUnderflow); // 0.0
}
}
Float MAX: 3.4028235E38
Float Overflow: Infinity
Double MAX: 1.7976931348623157E308
Double Overflow: Infinity
Float MIN (Positive): 1.4E-45
Float Underflow: 0.0
Double MIN (Positive): 4.9E-324
Double Underflow: 0.0
정수 연산
산술 연산을 정확하게 계산 하려면 실수 타입을 사용하지 않는 것이 좋음. 정확한 계산이 필요하면 정수 연산으로 변경 (double, float으로 정확한 실수 연산을 하는건 비추천, Math Class의 BigDecimal을 이용하는게 좋음)
나눗셈 연산에서 예외 방지하기
나눗셈 또는 나머지 연산에서 좌측 피연산자가 정수이고 우측 피연산자가 0일 경우 ArithmeticException 발생, 좌측 피연산자가 실수이거나 우측 피연산자가 0.0 또는 0.0f이면 예외가 발생하지 않고 연산의 결과는 무한대 또는 NaN이 됨
비트 이동 연산자
<< (왼쪽 시프트)
• 모든 비트(부호 비트 포함)를 왼쪽으로 이동.
• 오른쪽 빈자리는 항상 0으로 채움.
>> (부호 있는 오른쪽 시프트)
• 모든 비트를 오른쪽으로 이동, 부호 비트는 이동하지 않음.
• 왼쪽 빈자리는 부호 비트 값으로 채움 (1 또는 0).
>>> (부호 없는 오른쪽 시프트)
• 모든 비트를 오른쪽으로 이동, 부호 비트 포함.
• 왼쪽 빈자리는 항상 0으로 채움.
4조 workshop 토의 내용 기록
논의 1. 쇼트서킷이 뭘까? (feat. && || & |)
쇼트서킷이란? 논리연산자에서 좌측 피연산자 만으로도 결과가 확정된 경우, 굳이 우측 피 연산자의 계산 과정을 진행하지 않는 기능이다. 쇼트서킷의 연산 과정을 통해 불필요한 연산을 생략함으로써 성능적으로 이점을 볼 수 있다. 자바에서 || 이 대표적으로 쇼트서킷을 활용할수 있는 논리 연산자로 볼 수 있는데, 다음과 같은 예시에서 쇼트 서킷이 뭔지 알 수 있다.
public class Test {
public static void main(String[] args) {
int x = 10;
int y = 20;
x++; // x = 11
y--; // y = 19
if (x == 11 || y == 20) {
System.out.println("쇼트서킷을 통해 y값이 19이지만, 조건식의 연산이 최종적으로 true로 연산되어 출력문이 출력된다.");
} else {
System.out.println("쇼트서킷 실패");
}
}
}
그렇다면 비트 연산자인 &와 | 는 왜 쇼트서킷이 적용이 안될까? 그건 바로 비트 연산자는 좌항과 우항의 자릿수의 비트에 대한 비트연산을 적용하는 방식으로 동작하기 떄문이다.
0101 (a)
& 0011 (b)
------
0001 (결과: 1)
===================
0101 (a)
| 0011 (b)
------
0111 (결과: 7)
논의 2. yeild는 어디에 사용하는 키워드일까?
확인문제 2번의 정답 중 yield(반환하다, 돌려주다) 라는 키워드에 대한 논의를 진행하였다. 다음과 같은 예시에서 yield의 사용법을 엿볼수 있었는데, 간단하게 설명해보자면 switch case문에서 결과값을 반환할 때 사용하는 키워드 값이라고 볼 수 있다.
함수에 값을 돌려준다는 개념으로 접근해야하는데, return은 메서드를 종료하며 값을 반환하는 것이지만, yield는 switch case 문의 값을 시작하는 지점으로 값을 돌려준다.
public class Test {
public static void main(String[] args) {
String grade = "B";
int score = switch (grade) {
case "A" -> 100; // 단순 리터럴 반환
case "B" -> {
int deduction = 20; // 블록 내에서 연산 수행
int result = 100 - deduction;
yield result; // 연산 결과 반환
}
default -> 60; // 기본값 반환, yeild 사용시에 defult 필수
};
System.out.println(score); // 출력: 80
}
}
화살표(->)를 사용하여 단순 값을 반환할 때는 yield 없이 바로 값을 지정할 수 있다.
논의3. printf는 뭘까?
정수를 표현하여 출력할떄는 %d, 실수를 표현하여 출력할때는 %f, 문자를 표현하여 출력할때는 %s, 시간을 표현하여 출력하는 %t, 결국 printf의 사용은 지시자를 통해 변수의 값을 표현하는 개념이 적용된다.
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 6; j++) {
// 값 출력시에, i,j의 값을 정수값으로 표현해주는 서식
// %d: 정수, %s: 문자열 %t : 시간 등등
System.out.printf("%d %d ", i, j);
}
System.out.println();
}
}
}
논의4. 소수점 표현에 대한 이야기
소수를 표현하는 타입에는 float 과 double 이 있지만 정밀도의 문제로 float은 7자리까지, double은 15자리까지 표현된다(유효숫자). 하지만 정확한 소수관련된 데이터를 다루기 위해서는 Bigdecimal 클래스를 사용한 로직을 구성해야 정확한 계산을 진행할 수 있다. 왜냐하면 float과 double은 소수를 이진수로 표현하는 과정에서 대부분 무한 반복 소수가 되어 근사치 값으로 저장되기 때문이다.
예를들어 0.1을 2진수로 표현하려면 어떻게 표현되는지 아래 예시를 통해 알아보자.
0.1 * 2 = 0.2 (정수 부분은 2진수로 기록, 소수 부분은 다음 계산에 사용)
위 과정을 반복.... 아래와 같은 연산이 반복됨
위 과정의 반복이 종료되려면, 결과값이 0이 되어야 종료.
0.1 * 2 = 0.2 → 정수 부분 : 0
0.2 * 2 = 0.4 → 정수 부분 : 0
0.4 * 2 = 0.8 → 정수 부분 : 0
0.8 * 2 = 1.6 → 정수 부분 : 1
0.6 * 2 = 1.2 → 정수 부분 : 1
결론적으로 0.1을 2진수로 표현하려면 0.0001100...(무한반복)
0.1은 2진수로 정확히 표현되지 않고, 무한 반복 소수가 된다.
결론적으로 컴퓨터는 위의 무한반복되는 값을 근사값으로 저장한다.
Bigdecimal에 대한 사용을 진행하면 내부적으로 Integer 끼리의 연산을 진행하고 차후 마지막 결과값을 반환시에 어느 부분에서 소수점을 찍어야 하는지 처리되기때문에 정확한 연산이 가능하게 된다.
위와 같은 장점이 존재하지만, 단점으로는 많은 메모리와 오버헤드를 유발하기에 대량의 숫자 연산을 처리할 때 문제가 될 수 있다.
Touch background to close