String의 equals 메서드
String변수를 생성할때는 두가지 방법이 있습니다.
1, 리터럴을 이용한 방식
2, new 연산자를 이용한 방식
위에 두가지 방식에는 차이점이 존재하는데 리터럴을 사용하게 되면 string constant pool이라는 영역에 존재하게 되고 new를 통해 String을 생성하면 Heap 영역에 존재하게 됩니다.
String str1 = "apple"; //리터럴을 이용한 방식
String str2 = "apple"; //리터럴을 이용한 방식
String str3 = new String("example"); //new 연산자를 이용한 방식
String str4 = new String("example"); //new 연산자를 이용한 방식
java에는 주소값 비교(==)와 값 비교(equals)가 존재하는데 기본형 타입의 int, char, long 등은 Call by Value 형태로 기본적으로 대상에 주소값을 가지지 않는 형태로 사용되지만 String은 일반적인 타입이 아니라 클래스 타입입니다. 클래스는 기본적으로 Call by Reference형태로 생성 시 주소값이 부여됩니다. 그러므로 String타입을 선언했을때는 같은 값을 부여하더라도 서로간의 주소값이 다르기 때문에 (==)으로 비교하면 false가 나오는 것을 확인할 수 있습니다.
public class ThisIsJava {
public static void main(String[] args) {
사람 a사람1 = new 사람("홍길동", 22);
사람 a사람2 = new 사람("홍길동", 22);
if ( a사람1.equals("ㅋㅋ") )
{
System.out.println("거짓1");
}
//동치성
//동등성(둘의 스팩이 같냐?
if ( a사람1.equals(a사람2) )
{
System.out.println("참1");
}
}
}
class 사람 {
String 이름;
int 나이;
사람(String 이름, int 나이) {
this.이름 = 이름;
this.나이 = 나이;
}
@Override
public boolean equals(Object o) {
if ( o instanceof 사람 == false ) { // o가 연결된 것이 사람인지 알아보는 로직
return false;
}
// 추상적인 것에서 구체적인 것으로 바꿀경우 수동 형변환을 해준다.
사람 other = (사람)o;
if ( !이름.equals(other.이름) ) { // 이름이 같지 않다면 false 리턴
return false;
}
if ( this.나이 != other.나이 ) { // 나이가 같지 않다면 false 리턴
return false;
}
return true;
}
}
출처 : https://coding-factory.tistory.com/536
제네릭
제네릭 함수란?
"데이터를 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 이도록 하는 방법이다" 라고 생각하면 편하다. 제네릭 함수의 사용방법은 다음과 같다.
ArrayList<Integer> a저장소1 = new ArrayList<Integer>();
ArrayList<Double> a저장소1 = new ArrayList<>(); // 뒤에 괄호에서는 Double이 생략가능하다.
제일 중요한 것은 이렇게 들어가는 타입을 지정해 준다는 것이다.
아래의 코드를 보면 반복되는 코드가 많은 것을 알 수 있다. 타입이 달라 각 각 만들어야하는 메서드이기 때문이다. 지금은 3개라 쉽게 복사 붙여넣기로 작성할 수 있지만 이게 만약에 많아져 100개가 된다면 반복되는 코드를 계속 만들어야 한다. 그렇다면 이 귀찮은 과정을 어떻게 줄여나갈까? 이 방법을 알아보기 위해서는 제네릭 함수를 알아봐야 한다.
class Main {
public static void main(String[] args) {
Int저장소 a저장소1 = new Int저장소();
a저장소1.setData(30);
int a = a저장소1.getData();
System.out.println(a);
Double저장소 a저장소2 = new Double저장소();
a저장소2.setData(5.5);
double b = a저장소2.getData();
System.out.println(b);
사과저장소 a저장소3 = new 사과저장소();
a저장소3.setData(new 사과());
사과 c = a저장소3.getData();
System.out.println(c);
}
}
class Int저장소 {
Object data;
int getData() {
return (int)data;
}
void setData(Object inputedData) {
this.data = inputedData;
}
}
class Double저장소 {
Object data;
double getData() {
return (double)data;
}
void setData(Object inputedData) {
this.data = inputedData;
}
}
class 사과 {
}
class 사과저장소 {
Object data;
사과 getData() {
return (사과)data;
}
void setData(Object inputedData) {
this.data = inputedData;
}
}
아래 코드를 보면 제네릭 함수를 사용하여 메서드를 1개로 줄였다. 이렇듯 제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 확인할 수 있다. 한마디로 특정 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
public class ThisIsJava {
public static void main(String[] args) {
저장소<Integer> a저장소1 = new 저장소<>();
a저장소1.setData(30);
int a = a저장소1.getData();
System.out.println(a);
저장소<Double> a저장소2 = new 저장소<>();
a저장소2.setData(5.5);
double b = a저장소2.getData();
System.out.println(b);
저장소<사과> a저장소3 = new 저장소<>();
a저장소3.setData(new 사과());
사과 c = a저장소3.getData();
System.out.println(c);
}
}
class 저장소<T> {
Object data;
T getData() {
return (T)data;
}
void setData(Object inputedData) {
this.data = inputedData;
}
}
class 사과 {
}
제네릭의 장점
1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
출처 : https://st-lab.tistory.com/153
Stream
자바의 스트림은 Java8부터 지원하기 시작된 기능이다. 컬렉션에 저장되어 있는 요소들을 하나씩 순회하면서 처리할 수 있는 코드 패턴이다. 또한 Stream은 보통 람다식과 같이 사용된다.
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String[] inputBits = sc.nextLine().split(" ");
long sum = 0;
// 스트림을 사용하지 않고 작성하였을 경우
for (String inputBit : inputBits) {
sum += Long.parseLong(inputBit);
}
System.out.println(sum);
sc.close();
}
}
위의 코드는 스트림을 사용하지 않았을 경우의 예시이며 아래 코드는 스트림을 사용하였을 경우이다. 현재 코드는 매우 짧아 스트림을 사용하였을 경우에도 크게 코드량이 줄어들지 않지만 for문 많이지면 많아질수록 Stream과의 차이가 명확하게 보일거다.
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String[] inputBits = sc.nextLine().split(" ");
long sum = Arrays.stream(inputBits)
.mapToLong(e -> Long.parseLong(e))
.sum();
System.out.println(sum);
sc.close();
}
}
스트림에는 여러 기능이 있는데 우선 많이 사용하는 것에 대하여 알아보자!
ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e"));
list.stream()
.filter("e"::equals)
.forEach(System.out::println);
위 코드의 결과로 e만 도출되는 것을 확인할 수 있는데 filter()기능은 말 그대로 필터라고 보면된다. 원하는 결과만 걸러내어 보여줄 수 있다.
Stream<String> stream = names.stream()
.map(s -> s.toUpperCase());
Map은 기존의 Stream 요소들을 변환하여 새로운 Stream을 형성하는 연산이다. 위의 코드는 String을 요소들로 갖는 Stream을 모두 대문자 String의 요소들로 변환하고자 하는 코드이다. 이런경우 .map()은 반복문을 대신해주는 역활을 가지고 있다면 쉽게 생각할 수 있다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Main {
public static void main(String[] args) {
List<Integer> al = new ArrayList<>() {{
add(30);
add(10);
add(20);
}};
Collections.sort(al, (e2, e1) -> {
// return e2 > e1 ? 1 : -1; // v1 오름차순 정렬
// return e2 > e1 ? -1 : 1; // v2 내림차순 정렬
// return e2 - e1; // v3 오름차순 정렬
return e1 - e2; // v4 내림차순 정렬
});
Collections.sort(al, (e2, e1) -> e1 - e2);
System.out.println(al);
}
}
위 코드의 정렬방식을 이용하여 Stream에 정렬방식을 넣어줬더니 정렬된 것을 확인할 수 있었다.
devices = devices
.stream()
.sorted((e2, e1) -> e2.getId() - e1.getId())
.collect(Collectors.toList());
for (Device device : devices) {
System.out.printf("%d %d\n", device.getId(), device.getGas());
}
위의 코드를 아래와 같이도 수정할 수 있다.
String output = devices.stream()
.sorted(Comparator.comparing(Device::getId))
.map(e -> "%d, %d".formatted(e.getId(), e.getGas()))
.collect(Collectors.joining("\n"));
System.out.println(output);
int[] arr = new int[] {33, 2, 55, 4, 51, 6, 71};
int[] toArr = Arrays.stream(arr)
.filter(e -> e % 2 == 0) // 2로 나눠지는 몫이 0인 숫자만 출력
.map(e -> e * 2) // 출력된 숫자에 * 2
.toArray(); // 배열로 작성하였을 때
// .collect(Collectors.toList()) //Collectors.toList() 를 사용할려면 배열이 아니라 List로 받아야 사용 가능하다.
System.out.println(Arrays.toString(toArr));
Java Collection frameWork
int[] arr = new int[10] // 10
arr[0] = 10;
arr[1] = 20;
arr[2] = 30; // 2까지만 선언하게되면 나머지 3~9까지는 낭비되는 메모리가 발생하게 된다.
배열은 한번 선언된 상태로 실행하게되면 내용을 늘리거나 줄일 수 없다는 단점이 있다. 꼭 코드를 수정해야만 바꿀 수 있다. 하지만 이 단점을 ArrayList를 사용하여 보완할 수 있는데 ArrayList란 무엇일까?
ArrayList<Integer> al = new ArrayList<>();
al.add(10); // 0
al.add(20); // 1
al.add(30); // 2
System.out.println(al.get(0) + al.get(1) + al.get(2));
System.out.println(al.size()); // 가변 길이
ArrayList는 가변길이로 만약에 al.add(40); 을 넣게 된다면 size가 4가 나오는 것을 확인할 수 있다. 즉 실행상태에서도 값을 쉽게 변경할 수 있는 장점이 있으며 배열과 같이 낭비되는 메모리가 발생하지 않는다는 장점이 있다.
배열과의 차이점
public class IsJava {
public static void main(String[] args) {
List<Integer> ages = new ArrayList<>();
ages.add(20); // 0
ages.add(25); // 1
ages.add(30); // 2
// ages.remove(1);
System.out.printf("철수 나이 : %d \n", ages.get(0));
System.out.printf("영수 나이 : %d \n", ages.get(2));
System.out.printf("영희 나이 : %d \n", ages.get(1));
HashMap<String, Integer> agesMap = new HashMap<>();
agesMap.put("철수", 20);
agesMap.put("영희", 25);
agesMap.put("영수", 30);
System.out.printf("철수 나이 : %d \n", agesMap.get("철수"));
System.out.printf("영수 나이 : %d \n", agesMap.get("영수"));
System.out.printf("영희 나이 : %d \n", agesMap.get("영희"));
}
}