본문 바로가기

개발기술/Java

Java 코딩구현 심화 : 스트림,람다식

특수 클래스 : Inner Class

내부클래스 : 클래스 in 클래스 (클래스 안에 선언한 클래스)

 

 

내부클래스의 종류  : 정적 클래스 (static)

  • OuterClass는정적(static)이 될 수 없습니다. 때문에 정적클래스는 내부클래스만을 지칭합니다
  • 외부 클래스와 논리적 관계만 있음, 독립 사용 가능
  • 외부에서 직접생성가능 

내부클래스의 종류  : 인스턴스 내부 클래스 (non-static)

  • 외부 클래스의 인스턴스에 종속적인 내부 클래스입니다.  이러한 클래스는 외부 클래스의 인스턴스가 생성된 후에야 인스턴스화 될 수 있으며, 외부 클래스의 인스턴스 변수 및 메소드에 직접적으로 접근할 수 있는 권한을 가집니다. 주로 외부클래스 인스턴스의 필드멤버처럼 다루어지며, 인스턴스멤버들과의 작업을 위해 만들어짐.
  • 외부 클래스 인스턴스에 종속됨, 외부 접근 시 외부 인스턴스 필요
// static inner는 클래스 이름으로 직접 접근 가능
OuterClass.StaticInner si = new OuterClass.StaticInner();  // ✅ 가능

// non-static inner는 반드시 outer 인스턴스가 있어야 가능
OuterClass outer = new OuterClass();
OuterClass.Inner ni = outer.new Inner();  // ✅ 외부 인스턴스 필요

 

  • 인스턴스 내부 클래스에서 외부클래스의 유효범위 안에 존재하기 때문에 내부클래스에서는 외부클래스의 인스턴스를 통하지 않아도 외부 클래스 인스턴스 멤버에 접근가능함. 그 외 외부에서는 내부 클래스에  접근 불가함. 
  • 내부클래스가 외부클래스 '안에서만' 사용되는 클래스이기에, 굳이 바깥에 분리하여 둘필요가 없어 외부클래스의 멤버처럼 내부클래스를 캡슐화를 진행하는 개념이다. 
    • 원래 클래스에는 default와 public 밖에 사용되지 않는데, 내부클래스는 모든 접근제어자를 사용가능.
public class OuterClass {

    private String outerField = "I am outer field";

    public class InnerClass {
        public void printOuterField() {
            // 외부 클래스의 인스턴스 멤버에 직접 접근 가능 (this 없이도)
            System.out.println(outerField);
        }
    }

    public void run() {
        InnerClass inner = new InnerClass();
        inner.printOuterField();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.run();
    }
}

 

 

public class InnerRunnableMainV1 {
    public static void main(String[] args) {
        log("main() start");

        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        
        log("main() end");
    }

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            log( "run()");
        }
    }

 

 

  • 지역 클래스 (local class) : 클래스의 메소드 안에 클래스가 있는 경우이며 선언된 내부에서만 사용가능하다.
  • 익명 클래스 (anonymous class) : 부모(혹은 인터페이스) 로부터 상속을 통한 클래스의 선언과 인스턴스 생성이 동시에 되는 이름없는 일회용 클래스
    • 사용법 :  자신의 이름은 없고 상속을 받는 부모나 인터페이스의 이름을 사용하여 개체를 생성하고 생성과 동시에 클래스의 내용을 채워넣는다.
public class InnerRunnableMainV2 {
    public static void main(String[] args) {
        log("main() start");

        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                log("run() start");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

        log("main() end");
    }
}

 

 

 

람다식과 함수형 인터페이스

  • 람다식 : 함수형 인터페이스(함수를 객체로 표현하기 위한 인터페이스)를 기반으로, 익명 클래스(무명 클래스)를 간결하게 표현한 문법
    • 자바에서는 함수는 클래스에 독립적일 수 없기 때문에, 람다식은 결국 익명 클래스 인스턴스를 표현하는 것이다.
      • 익명클래스를 생성하는 문법이 생략되어있다고 생각하면 됨. 이 객체를 담는 참조변수가 필요하고 이를 위해서 함수형 인터페이스가 도입된다.
    • 함수형 인터페이스는 “메서드 하나를 구현하는 객체”를 기다리는 것이고,람다식은 그 객체를 생성해주는 표현입니다.
  • 람다식의 생략 규칙
    • 반환 타입 생략 → 컴파일러가 함수형 인터페이스의 메서드 시그니처로부터 추론
    • 메서드 이름 생략→ 이미 함수형 인터페이스에 정해져 있음
    • 매개변수 타입 생략 가능 → 타입 추론이 가능한 경우
    • 중괄호 {} 생략 가능 → 실행문이 한 줄일 경우

람다식 생략예시

From 

  • 추상화된 예시
type InterfaceName(parameter type a, b) { overriding method statement; }
  •  구체적인 예시
interface Adder {
    int add(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Adder adder = new Adder() {
            @Override
            public int add(int a, int b) {
                return a + b;
            }
        };

        System.out.println(adder.add(3, 5)); // 출력: 8
    }
}

 

To

(a, b) -> overriding method statement
interface Adder {
    int add(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Adder adder = (a, b) -> a + b;

        System.out.println(adder.add(3, 5)); // 출력: 8
    }
}

 

public class InnerRunnableMainV4 {
    public static void main(String[] args) {
        log("main() start");

        Thread thread = new Thread(() -> log("run() start"));
        thread.start();

        log("main() end");
    }
}

 

함수형인터페이스

  •  함수형인터페이스 : 단 하나의 추상메서드만 선언된 인터페이스이며 @Fuctional Interface라는 annotaion을 사용가능하다.
    • 함수형인터페이스의 역할은 람다식을 호출하기 위해서 함수형인터페이스가 method의 이름을 대신 선언해주는 것이라고  이해하면 됨
      • 익명클래스를 사용한 원형 MyFunction f = new public abstract int max ( int a, int b) {return a>b ? a : b ; }
      • 람다식을 사용한 축약형  MyFunction f = (a ,b) -> a>b ? a: b;
        • ex :  Comparator interface is a functional interface used to order the elements

 

  •  대표적인 함수형 인터페이스 ; 
    •  Predicate<T>  :  입력값 T를 받아 boolean을 반환하는 함수형 인터페이스. 
      • 주요메서드 : boolean test(T t): 이 메소드는 객체 T를 매개변수로 받아, 정의된 조건에 따라 true 또는 false를 반환함
      • 부가메서드
        • and(Predicate<? super T> other): 두 개의 Predicate 조건을 논리적 AND 연산으로 결합합니다.
        • or(Predicate<? super T> other): 두 개의 Predicate 조건을 논리적 OR 연산으로 결합합니다.
        • negate(): Predicate 조건의 논리적 부정(NOT)을 수행합니다.
      • 람다식 표현 : Predicate<String> startsWithA = name -> name.startsWith("A");
    • Comparable<T> : 객체가 자신의 비교 규칙을 내장하여 자연스러운 정렬 순서를 제공할 수 있게 해주는 인터페이스. .sort(object) 호출은 클래스의 compareTo 메소드를 내부적으로 사용하여 각 Person 인스턴스를 나이에 따라 정렬합니다.
      • int compareTo(T o): 이 메소드는 객체 자신(this)를 매개변수로 전달된 객체 o와 비교합니다. 이 메소드의 반환 값은 다음과 같은 세 가지 경우 중 하나입니다:
        • 음수 반환: 객체 자신이 매개변수 객체보다 "작다"는 것을 의미합니다.
        • 0 반환: 두 객체가 같다는 것을 의미합니다.
        • 양수 반환: 객체 자신이 매개변수 객체보다 "크다"는 것을 의미합니다.
public class Person implements Comparable<Person> {
    private String name;
    private int age;

    @Override
    public int compareTo(Person other) {
        // 나이를 기준으로 비교
        return Integer.compare(this.age, other.age);
    }
}
    •  Comparato<T>  :  두 객체를 외부에서 주어진 기준으로 비교하는 데 사용됩니다.  객체의 기본 정렬 순서(Comparable 인터페이스를 통한 정렬)와 다른 방식으로 객체를 정렬하고 싶을 때 유용합니다.
      • 주요메서드 : int compare(T o1, T o2): 이 메소드는 두 객체 o1과 o2를 비교하고, o1이 o2보다 작으면 음수를, 같으면 0을, 크면 양수를 반환합니다. 이 반환 값은 Collections.sort()나 Arrays.sort() 같은 정렬 메서드에서 사용되어 객체들을 정렬하는 데 활용됩니다.
      • 추가메서드
        • reversed(): 현재 Comparator의 반대 순서로 비교하는 새 Comparator를 반환합니다.
        • thenComparing(Comparator<? super T> other): 현재 Comparator로 두 객체가 같다고 판단될 때, other Comparator를 사용하여 추가 비교를 수행합니다.
          • thenComparing을 안쓰더라도 if문으로 비교결과값 ==0이면 새로운 비교로직을 적용하면 thencomparing을 쓰지않고서도 2가지이상 비교조건을 구현할 수 있음.
        • static <T> Comparator<T> naturalOrder(): 객체의 자연적인 정렬 순서(Comparable을 구현한 경우)에 따라 비교하는 Comparator를 반환합니다.
        • static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)  nullsLast(Comparator<? super T> comparator): null 값을 갖는 객체들을 각각 비교 시작이나 끝에 배치하는 Comparator를 반환합니다.
        • Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor ) : comparing <T>은 주로 람다 표현식이나 메소드 참조를 인자로 받습니다. 이 인자는 객체에서 특정 키를 추출하는 함수로 작동합니다. 추출된 키는 자연 순서(Comparable을 구현해야 함)에 따라 비교됩니다
  •  
    •  Supplier<T> : 매개변수로 받는 값은 없고 T값을 반환함.
      • Method Signature: The Supplier<T> interface has a single method, get(), which does not take any parameters and returns a value of type T:
      • Supplier<LocalDate> todaySupplier = LocalDate::now;
    • Consumer<T> : 매개변수로 T를 받고, 특정 행위를 수행한 후 반환값은 없음
      • Consumer<String> printConsumer = System.out::println;
    • Function<T R> : 일반적인 함수로 인자 T를 받고 R값을 반환함.
      • Function<String, Integer> lengthFunction = String::length;
      • UnaryOperator<T>  :  operates on a single operand and returns a result of the same type as its operand. extention of Function<T, T>.
  • 메서드참조 : 하나의 메서드만을 호출하는 람다식은 메서드참조로 더 간단히 할 수 있다. 파라미터부분을 제거하고 사용되는 메서드는 문맥으로 프로그램은 파라미터에 대한 정보를 읽어들인다. 단, 아래의 불가예시와 같이 특정 인자를 사용해야되는 경우에는 람다식으로만 표현가능한 부분이 있음.
    • ex 1 : (x) -> ClassName.method(x) 에서 ClassName::method로 변환
    • ex 2 : (obj,x) -> obj.method(x) 에서 ClassName ::method로 변환
    • 불가 : name -> name.startsWith("A") 에서 String::startsWith("A) ; 메서드참조에 매개변수부분은 넣을수 없음.

Comparator 인터페이스는 주로 다음 메소드를 정의합니다:

  • int compare(T o1, T o2): 이 메소드는 두 객체 o1과 o2를 비교하고, o1이 o2보다 작으면 음수를, 같으면 0을, 크면 양수를 반환합니다. 이 반환 값은 Collections.sort()나 Arrays.sort() 같은 정렬 메서드에서 사용되어 객체들을 정렬하는 데 활용됩니다.

Comparator 인터페이스는 주로 다음 메소드를 정의합니다:

  • int compare(T o1, T o2): 이 메소드는 두 객체 o1과 o2를 비교하고, o1이 o2보다 작으면 음수를, 같으면 0을, 크면 양수를 반환합니다. 이 반환 값은 Collections.sort()나 Arrays.sort() 같은 정렬 메서드에서 사용되어 객체들을 정렬하는 데 활용됩니다.

 

스트림

 

  •   Stream은 배열이나 컬렉션과 같은 데이터 소스를 표준화된 방식으로 처리하기 위한 객체입니다.내부적으로 데이터를 하나씩 참조하며 처리하는 파이프라인 구조로 동작하며, 간결한 for문처럼 사용할 수 있습니다. 주로 람다식과 함께 사용되어, 데이터를 필터링하거나 변환, 집계하는 작업을 효율적으로 수행할 수 있습니다.
  •  스트림은 크게 3가지 요소로 구성됨
    • 1. Stream 생성  2. 중개 연산  3. 최종 연산
    • 데이터소스객체.Stream생성().중개연산().최종연산();

 

스트림 생성

컬렉션/배열에서 스트림 생성:

  • 객체 배열 스트림: Stream<Object> stream = Arrays.stream(arr);
  • 기본형 배열 스트림 (예: int[]) : IntStream stream = Arrays.stream(arr, fromIndex, toIndex);
  • 컬렉션에서 스트림: CollectionInstance.stream()
    • 객체 배열 스트림은 sum(), max() 등의 연산 메서드가 없음 → 직접 reduce 연산 필요
    • 기본형 배열 스트림 (IntStream, LongStream 등)은 sum(), average(), max() 등 집계 메서드를 지원하므로, 계산이 필요할 때 추천

스트림 빌더 사용:

  • Stream<Object> stream = Stream.builder().add(element1).add(element2)...build();
  • 타입을 지정한 빌더: Stream<Object> stream = Stream.<Object>builder().add(element).build();

무한 스트림 생성:

  • Stream.generate(Supplier Interface) 메소드를 사용하여 무한 스트림(요소가 무한히 생성되는 스트림)을 만들 수 있습니다.
  • 예: Stream.generate(() -> "ContentsToRepeat");

 

2. 중간연산

  중간 연산은 스트림의 입력과 출력이 모두 스트림인 연산입니다. 즉, 중간 연산을 여러 번 반복적으로 적용할 수 있으며, 최종 연산이 수행될 때까지 실제 실행되지 않습니다.

 

  • filter(Predicate<T>) - 조건이 참인 요소들만 걸러내는 메소드입니다.
IntStream.range(1,N+1).filter(i->i%2==1).toArray();
  • IntStream.range (a,b) : a부터 b-1까지의 숫자 스트림을 생성, b는 포함하지않음 
    • IntStream.range(1, 10).filter(n -> n % 2 == 0);
  • IntStream.limit(long maxSize) : 스트림의 요소를 특정개수까지만 제한하는 메소드
    • IntStream.range(1, 100).limit(5); // 1부터 5까지의 스트림 생성
  • sorted(): 스트림 내 요소를 정렬합니다. 기본적으로 오름차순으로 정렬됩니다.
  • map (function 연산) : 스트림의 각 요소에 대해 특정 연산을 수행후 새로운 스트림 Stream<T> 을 반환하는 메소드입니다.  
    • Stream<T>형태로 반환하기 때문에 primitive data를 결과로 산출하는 function은 WrapperClass로 autoboxing되기때문에 mapToInt를 사용해야함.
Stream<String> strStream = Stream.of("1", "2", "3");
Stream<Integer> intStream = strStream.map(Integer::parseInt); // String → Integer
  • mapToInt(Integer::parseInt) : Stream<T> 를 IntStream 으로 변환
    • Stream<String> strStream = Stream.of("1", "2", "3");
    • strStream.mapToInt(Integer::parseInt); // 문자열을 정수로 변환
Stream<String> strStream = Stream.of("1", "2", "3");
IntStream intStream = strStream.mapToInt(Integer::parseInt); // String → int

intStream.forEach(System.out::println);
  • boxed() : 기본형(primitive) 데이터 타입을 래퍼(wrapper) 클래스로 변환합니다..
    • Arrays.stream(new int[]{1, 2, 3}).boxed().collect(Collectors.toList()); // int 배열을 리스트로 변환

 

3. 최종연산 :

  연산결과가 스트림이 아닌 연산, 스트림은 사용되면 소모되기때문에 단 한번만 사용가능

  • Primitive type 생성 (Primitive type stream 전용 )
    • sum() - count() - average() - min()- max()
  • 객체생성
    • toArray() : create an array of Object or an array of a specific type via an array constructor reference.
      • ex : String[] array = stream.toArray(String[]::new);
    • collect()  :  스트림의 요소들을 하나의 결과물로 모으는 terminal operation. 주로 Collectors 유틸리티 클래스의 메서드를 함께 사용
      • ex : stream.collect(Collectors.toList()); stream.collect(Collectors.toSet());
예시 설명
toList() 스트림 → List<T>
toSet() 스트림 → Set<T>
toMap() 스트림 → Map<K, V>
joining() 스트림의 문자열 요소들을 연결
counting() 요소 개수 세기
groupingBy() 특정 기준으로 그룹핑
partitioningBy() 조건에 따라 true/false 두 그룹으로 나누기

 

  • 공통 
    • foreach(Consumer Interface- Class::method) :  stream을 소모하여 class의 method를 실행시킴.
      • consumer functional interface를 parameter로하여
      • iterable interface에서는 stream interface에서처럼 foreach 가 정의되어있어 각 iteam에 대해서 consumer Interface를 실행시킬 수 있다.
weatherResponse.getBody().getItems().getItem().forEach(item -> {if(item.getFcstValue()==null) throw new RuntimeException();});

 

  • reduce : combine all elements of the stream into a single result. It's a model of a folding operation, where a binary operator is applied repeatedly to combine elements successively until a single value remains. first parameter is an initial value for the reduction process and as a default result if the stream is empty.
    • reduce(1, (a, b) -> a * b);
  • Optional<T> findAny() : Stream에서 아무 요소 중 아무거나 하나를 반환. filter(Predicate<T>)와 같이 많이 사용됨

 

 

'개발기술 > Java' 카테고리의 다른 글