인터페이스 기본 메소드와 스태틱 메소드

기본메소드

  • 인터페이스에 메소드 선언이 아니라 구현체를 제공하는 방법
  • 해당 인터페이스를 구현한 클래스를 깨트리지 않고 새 기능을 추가할 수 있다.
  • 기본 메소드는 구현체가 모르게 추가된 기능으로 그만큼 리스크가 있다.
    • 컴파일 에러는 아니지만 구현체에 따라 런타임 에러가 발생할 수 있다. (nullpointException과 같은)
    • 반드시 문서화 할 것 (@implSpec 자바독 태그 사용)
  • Object가 제공하는 기능 (equals, hasCode)은 기본 메소드로 제공할 수 없다.
    • 구현체가 재정의해야 한다.
  • 본인이 수정할 수 있는 인터페이스에만 기본 메소드를 제공할 수 있다.
  • 인터페이스를 상속받는 인터페이스에서 다시 추상 메소드로 변경할 수 있다.
  • 인터페이스 구현체가 재정의 할 수도 있다.

 

스태틱 메소드

  • 해당 타입 관련 헬터 또는 유틸리티 메소드를 제공할 때 인터페이스에 스태틱 메소드를 제공할 수 있다. 

 

위의 내용들을 코드로 이해해보자.

package me.qazyj.java8;

public interface FooInterface {
    void printName();

    /**
     * @implSpec
     * 이 구현체는 getName()으로 가져온 문자열을 대문자로 바꿔 출력한다.
     */
    default void printNameUpperCase(){
        System.out.println(getName().toUpperCase());
    }

    String getName();
}
package me.qazyj.java8;

public class DefaultFoo implements FooInterface{
    String name;

    public DefaultFoo(String name){
        this.name = name;
    }

    // 재정의도 가능하다.
    @Override
    public void printNameUpperCase(){
        System.out.println(getName().toUpperCase());
    }

    @Override
    public void printName() {
        System.out.println(this.name);
    }

    @Override
    public String getName() {
        return name;
    }


}

위의 코드에서 기본 메소드란 printNameUpperCase()를 말한다. name을 대문자로 출력하는 기능을 추가했지만, 인터페이스에만 코드를 추가해주면 된다. (default 선언 필수!! 하지 않으면 컴파일 에러가 발생한다.) 물론, 재정의도 가능하다. 하지만, 이전에 리스크가 있다는 말을 했다. 이유는 name이 Null값인 경우 NullPointException과 같은 에러가 발생할 수 있다. 개발자도 사람인지라 정리를 잘 해놓지 않으면 후에 에러를 알게되었을 때 찾기 쉽지 않을 수 있다. (꼭! 문서화 하여 정리하자! 정리 방법은 위의 코드를 보면 예시가 있다.)

 

그렇다면, 두개의 interface를 구현하는 구현체가 있다고 가정해보자. 두개의 interface에는 같은 이름으로된 기본메소드가 있다면 어떻게 될까?? 당연히 컴파일에러가 발생한다. 해결 방법으로는 간단하다. 하나의 인터페이스 기본메소드를 없애주거나, 구현체에서 기본메소드를 재정의해주면 된다. 

 

Object가 제공하는 기능 (equals, hasCode)은 기본 메소드로 제공할 수 없다고했다. 직접 확인해보자.

기본메소드로 선언하면 컴파일 에러가 뜬다.
구현체에서 재정의해서는 사용할 수 있다.

위를 보면 기본메소드로는 선언이 불가하고, 구현체에서 재정의해서는 사용할 수 있는 것을 볼 수 있다.

 

 

static 메소드는 어떻게 사용할까.

package me.qazyj.java8;

public interface FooInterface {
    
    ...

    static void printAnything() {
        System.out.println("kyj");
    }

    ...
}
package me.qazyj.java8;

public class FooTest {
    public static void main(String[] args) {
        DefaultFoo foo = new DefaultFoo("kyj");
        foo.printName();
        foo.printNameUpperCase();

        FooInterface.printAnything();		// 생성해주지 않아도 바로 사용가능하다.
    }
}

static으로 선언해주면 별다른 생성없이 바로 사용할 수 있는 것을 볼 수 있다.

 

 

 

 

자바 8에서 추가한 기본 메소드로 인한 API 변화

Iterable의 기본 메소드

  • forEach()
  • spliterator()

 

Collection의 기본 메소드

  • stream() / parallelStream()
  • removeIf(Predicate)
  • spliterator()

 

Comparator의 기본 메소드 및 스태틱 메소드

  • reversed()
  • thenComparing()
  • static reverseOrder() / naturalOrder()
  • static nullsFirst() / nullsLast()
  • static comparing()

위의 내용을 코드로 이해해 보자.

 

    public static void main(String[] args) {

        List<String> name = new ArrayList<>();
        name.add("kyj");
        name.add("yj");
        name.add("qazyj");


        System.out.println("=====Iterable=====");
        // Iterable
        name.forEach(System.out::println);		// 출력 kyj + "\n" + yj + "\n" + qazyj

        System.out.println();

        Spliterator<String> spliterator = name.spliterator();
        Spliterator<String> spliterator2 = spliterator.trySplit();// 앞에서부터 반으로 쪼갠걸 가져옴 (나머지 반은 원래 변수에)
        while(spliterator2.tryAdvance(System.out::println));	// 출력 kyj
        System.out.println("==========");
        while(spliterator.tryAdvance(System.out::println));		// 출력 yj + "\n" + qazyj



        System.out.println("=====Collection=====");
        //Collection
        name = new ArrayList<>();
        name.add("kyj");
        name.add("yj");
        name.add("qazyj");

        long count = name.stream().map(String::toUpperCase)
                .filter(s -> s.startsWith("K"))
                .count();
                //.collect(Collectors.toSet());
        System.out.println(count);		// 출력 1   (kyj 하나만 있기 때문) 

        name.removeIf(s -> s.startsWith("k"));
        name.forEach(System.out::println);		// 출력 yj + "\n" + qazyj


        System.out.println("=====Comparator=====");
        //Comparator
        name = new ArrayList<>();
        name.add("kyj");
        name.add("yj");
        name.add("qazyj");

        name.sort(String::compareToIgnoreCase); // 오름차순
        Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase;
        name.sort(compareToIgnoreCase.reversed());  // 내림차순 추가적으로 더 정렬하고 싶으면 .thenComparing() 사용
        

    }

 

 

 

 

 

'JAVA' 카테고리의 다른 글

[Java 8] Optional  (0) 2022.10.07
[JAVA 8] Stream  (0) 2022.10.06
[JAVA 8] 함수형 인터페이스와 람다 표현식  (0) 2022.09.23
ListIterator  (0) 2022.09.23
SnakeGame  (0) 2022.07.16

함수형 인터페이스 (Functional Interface)

  • 추상 메소드를 1개만 가지고 있는 인터페이스
  • SAM (Single Abstract Method) 인터페이스
@FunctionalInterface    // 자바가 제공해주는 functionalInterface (인터페이스를 견고하게 관리)
public interface RunSomthing {

    void doIt();    // 하나만 있으면 함수형 인터페이스 (2개이상부터 X)
}

@FunctionalInterface 어노테이션을 추가하면 함수형 인터페이스로 선언해줍니다. (추상 메소드를 하나 더 추가하려하면 개발자가 잘 못된 것을 알려줍니다.)  (이를통해, 디버깅 전에 에러를 확인할 수 있도록합니다.)

추상 메소드를 2개이상하는 경우 에러 표시

 

 

 

람다 표현식 (Lamda Expressions)

  • 함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.
  • 코드를 간결하게 할 수 있다.
  • 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수 있다.
        // 익명 내부 클래스 (함수형 인터페이스가 아닌 경우)
        RunSomthing runSomthing = new RunSomthing() {
            @Override
            public void doIt() {

            }
        };

        // 함수형 인터페이스의 경우 java 8 부터 람다로 변경 가능
        RunSomthing runSomthingLamda = () -> System.out.println("Lamda");

함수형 인터페이스의 경우 람다 표현식으로 이용해 코드를 훨씬 간결하게 만들 수 있는 것을 볼 수 있다. 

 

 

 

자바에서 함수형 프로그래밍

  • 함수를 First class object로 사용할 수 있다.
  • 순수 함수 (Pure Function)
    • 사이드 이팩트가 없다. (함수 밖에 있는 값을 변경하지 않는다.) (같은 값을 넣으면 같은 값으로 결과가 나와야함)
    • 상태가 없다. (함수 밖에 있는 값을 사용하지 않는다.)
  • 고차 함수 (Higher-Order Function)
    • 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
  • 불변성

 

 

Java에서 제공하는 기본 함수형 인터페이스

 

java.util.function (Java Platform SE 8 )

Interface Summary  Interface Description BiConsumer Represents an operation that accepts two input arguments and returns no result. BiFunction Represents a function that accepts two arguments and produces a result. BinaryOperator Represents an operation u

docs.oracle.com

  1. Function<T,R>
    • T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
      • R apply(T t)
      • Function<Integer, Integer> plus10 = (i) -> i+10;  과 같이 람다를 이용해 쉽게사용할 수 있다.
    • 함수 조합용 메소드
      • andThen
      • compose
  2. BiFunction<T,U,R>
    • 두 개의 값 (T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스 (Function에서 파라미터만 추가 된 느낌)
      • R apply(T t, U u)
  3. Consumer<T>
    • T 타입을 받기만 하는 함수 인터페이스 (리턴이 없다.)
      • void accept(T t)
  4. Suplier<T>
    • T 타입의 값을 제공하는 함수 인터페이스
      • T get()
  5. Predicate<T>
    • T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
      • boolean test(T t)
  6. UnaryOperator<T>
    • Function<T,R>의 특수한 형태로 입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스 (Function<T,T> 느낌)
  7. BinaryOperator<T>
    • BiFunction<T,R,U>의 특수한 형태로 동일한 타입의 입력값 두개를 받아 리턴하는 함수 인터페이스 (BiFunction<T,T,T> 느낌)
  8. ..... 등등 다양한 함수형 인터페이스들을 제공해 주고있다.

 

 

 

 

람다 표현식의 Scope

private void run() {
    final int baseNumber = 10;

    // 로컬 클래스
    class LocalClass {
        void printBaseNumber() {
            System.out.println(baseNumber);
        }
    }

    // 익명 클래스
    Consumer<Integer> integerConsumer = new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(baseNumber);
        }
    };

    // 람다
    IntConsumer printInt = (i) -> {
        System.out.println(i+baseNumber);
    };

    printInt.accept(10);
}

run 메소드에서 로컬클래스, 익명클래스, 람다를 통해 run 메소드에서 정의한 변수를 사용하면 정상적으로 사용하는 것을 볼 수 있었다. 하지만 로컬클래스, 익명클래스와 람다는 다른 점이 하나 있다. 로컬클래스와 익명클래스는 쉐도잉이 되는 반면, 람다는 쉐도잉이 되지 않는다는 것이다. 그렇다면 쉐도잉이 무엇일까??

말하자면 그림과 같다. 위의 코드를 보면 run 에서 baseNumber이라는 변수를 선언했다. 그 후 baseNumber라는 변수를 로컬클래스, 익명클래스에서 사용한다면 새로운 값으로 대체할 수 있다. 하지만, 람다의 경우에는 같은 변수명을 사용할 수 없다는 에러가 발생한다. 

실제 람다에서 변수를 선언했을 때 나는 에러

그렇다면 이유는 무엇일까?? 이유는 로컬클래스와 익명클래스는 Scope이 선언해주는 곳과 같지 않기때문이고, 람다의 경우는 Scope이 선언해주는 곳과 같기 때문이다.

 

 

 

메소드 레퍼런스

람다가 하는 일이 기존 메소드 또는 생성자를 호출하는 거라면, 메소드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있다.

메소드 참조하는 방법

스태틱 메소드 참조 타입::스태틱 메소드
특정 객체의 인스턴스 메소드 참조 객체 레퍼런스::인스턴스 메소드
임의 객체의 인스턴스 메소드 참조 타입::인스턴스 메소드
생성자 참조 타입::new
  • 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.
  • 리턴값 또는 생성한 객체는 람다의 리턴값이다.

예를들어 보겠다. 먼저 아래와 같은 Greeting이라는 클래스가 있다.

package me.qazyj.java8;

public class Greeting {

    private String name;

    public Greeting() {}

    public Greeting(String name){
        this.name = name;
    }

    public String hello(String name) {
        return "hello "+ name;
    }

    public static String hi(String name){
        return "hi " + name;
    }
}

사용한 예시를 보면 아래와 같다.

    public static void main(String[] args) {
        // static 메소드
        UnaryOperator<String> hi = Greeting::hi;

        // non-static 메소드
        //UnaryOperator<String> hi = Greeting::hello; (불가)
        Greeting greeting = new Greeting();
        UnaryOperator<String> hello = greeting::hello;

        // 임의 객체의 인스턴스 메소드 참조
        String[] names = {"k", "y", "j"};
        Arrays.sort(names, String::compareToIgnoreCase);//타입::인스턴스 메소드

        // 입력값이 없는 생성자로 생성
        Supplier<Greeting> newGre = Greeting::new;

        // 입력값이 있는 생성자로 생성
        Function<String, Greeting> kyjGree = Greeting::new;
        Greeting kyj = kyjGree.apply("kyj");
    }

 

'JAVA' 카테고리의 다른 글

[Java 8] Optional  (0) 2022.10.07
[JAVA 8] Stream  (0) 2022.10.06
[JAVA 8] 인터페이스의 변화  (0) 2022.10.06
ListIterator  (0) 2022.09.23
SnakeGame  (0) 2022.07.16

List 인터페이스의 경우 Collection의 구현체 외 iterator , add , remove , equals  hashCode 메소드 의 계약에 추가 규정을 둡니다. 편의를 위해 다른 상속된 메서드에 대한 선언도 여기에 포함됩니다. 

 

오늘 공부할 것은 Iterator에서 ListIterator에 관한 것 입니다. 

 

ListIterator은 아래와 같이 커서(^)와 같은 위치 값을 갖고 있습니다. 이를 next()와 previous()를 통해 움직입니다. hashNext(), hashprevious()를 통해 다음값이 있으면 true, 없다면 false를 반환 받습니다. 또한, add(E), remove(), set(E)를 통해 값을 넣고, 빼고, 바꿀 수 있습니다.

위와 같이보면 위치를 가리키고있는 것을 제외하면 ArrayList, LinkedList와 유사하다고 생각합니다.

 

 

시간복잡도는 어떨까요. 먼저, ArrayList와 LinkedList의 시간복잡도 입니다.

  Method ArrayList LinkedList
add at last index add() O(1) O(1)
add at given index add(index, value) O(N) O(1)
remove by index remove(index) O(N) O(1)
remove by value remove(value) O(N) O(1)
get by index get(index) O(1) O(N)
search by value indexOf(value) O(N) O(N)

추가, 삭제 시 ArrayList보다 LinkedList가 훨씬 빠릅니다. 하지만, LinkedList의 경우 index로 맨앞, 맨뒤가 아닌 중간에 값을 추가, 삭제할 경우에 탐색해야하기 때문에 사실상 O(N)이 걸린다고 봐야합니다.

 

결론적으로 중간에 데이터를 추가, 삭제하는 작업이 많은 경우 ArrayList, LinkedList 모두 좋은 자료구조가 아니라고 볼 수 있습니다. 

 

index를 이동하며, 데이터를 추가, 삭제해야하는 경우에 적합한 자료구조는 바로 ListIterator입니다.

 

ListIterator의 경우 index를 한칸씩 이동하는 커서가있고 previous(), next() 메소드를 가지고 있습니다. 또한, List 인터페이스가 LinkedList 구현체로 되어있다면 추가, 삭제에 O(1)의 시간복잡도를 사용합니다. (이 부분이 이해되지 않는다면 ArrayList와 LinkedList의 차이점을 공부해봅시다.)

 

 

위의 개념을 완벽하게 이해하고있다면 쉽게 풀 수 있는 문제가 있습니다. 

 

1406번: 에디터

첫째 줄에는 초기에 편집기에 입력되어 있는 문자열이 주어진다. 이 문자열은 길이가 N이고, 영어 소문자로만 이루어져 있으며, 길이는 100,000을 넘지 않는다. 둘째 줄에는 입력할 명령어의 개수

www.acmicpc.net

 

 

 

참고

listIterator 공식문서

 

Java Platform SE 8

 

docs.oracle.com

Iterator 공식문서

 

Iterator (Java Platform SE 8 )

An iterator over a collection. Iterator takes the place of Enumeration in the Java Collections Framework. Iterators differ from enumerations in two ways: Iterators allow the caller to remove elements from the underlying collection during the iteration with

docs.oracle.com

list 공식문서

 

List (Java Platform SE 8 )

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the lis

docs.oracle.com

 

'JAVA' 카테고리의 다른 글

[Java 8] Optional  (0) 2022.10.07
[JAVA 8] Stream  (0) 2022.10.06
[JAVA 8] 인터페이스의 변화  (0) 2022.10.06
[JAVA 8] 함수형 인터페이스와 람다 표현식  (0) 2022.09.23
SnakeGame  (0) 2022.07.16

우연히 youtube를 보다가 java로 만든 뱀 게임이 있길래 새벽에 심심해서 따라해 봤다...하하하하하

 

영상의 길이는 40분이 살짝 넘어서 따라해볼만 한 것 같았다.

 

 

혹시나 궁금해하는 분들이 있을 수도 있으니 아래 링크와 코드의 흔적을 남겨놓겠다,,, 간단하면서 나름 재밌는 프로그램이니 따라해봐도 좋을 것??? 같다

 

 

Main

public class SnakeGame {
    public static void main(String[] args) {
        GameFrame frame = new GameFrame();
    }
}

Frame

import javax.swing.*;

public class GameFrame extends JFrame {
    GameFrame(){

        this.add(new GamePanel());
        this.setTitle("Snake");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setResizable(false);
        this.pack();
        this.setVisible(true);
        this.setLocationRelativeTo(null);
    }
}

Panel

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Random;

public class GamePanel extends JPanel implements ActionListener {

    static final int SCREEN_WIDTH = 600;
    static final int SCREEN_HEIGHT = 600;
    static final int UNIT_SIZE = 25;
    static final int GAME_UNITS = (SCREEN_WIDTH*SCREEN_HEIGHT)/UNIT_SIZE;
    static final int DELAY = 75;
    final int x[] = new int[GAME_UNITS];
    final int y[] = new int[GAME_UNITS];
    int bodyParts = 6;
    int applesEaten;
    int appleX, appleY;
    char direction = 'R';
    boolean running = false;
    Timer timer;
    Random random;

    GamePanel(){
        random = new Random();
        this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        this.setBackground(Color.black);
        this.setFocusable(true);
        this.addKeyListener(new MyKeyAdapter());
        startGame();
    }

    public void startGame() {
        newApple();
        running = true;
        timer = new Timer(DELAY, this);
        timer.start();
    }

    public void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        draw(graphics);
    }

    public void draw(Graphics graphics){

        if(running) {
            /*
            for (int i = 0; i < SCREEN_HEIGHT / UNIT_SIZE; i++) {
                graphics.setColor(Color.GRAY);
                graphics.drawLine(i * UNIT_SIZE, 0, i * UNIT_SIZE, SCREEN_HEIGHT);
                graphics.drawLine(0, i * UNIT_SIZE, SCREEN_WIDTH, i * UNIT_SIZE);
            }*/
            graphics.setColor(Color.red);
            graphics.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);

            for (int i = 0; i < bodyParts; i++) {
                if (i == 0) {
                    graphics.setColor(Color.green);
                    graphics.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
                } else {
                    //graphics.setColor(new Color(45, 180, 0));
                    graphics.setColor(new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)));
                    graphics.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
                }
            }

            // Score Board
            graphics.setColor(Color.red);
            graphics.setFont(new Font("Ink Free", Font.BOLD, 40));
            FontMetrics metrics = getFontMetrics(graphics.getFont());
            graphics.drawString("Score : " + applesEaten,
                    (SCREEN_WIDTH - metrics.stringWidth("Score : " + applesEaten))/2,
                    graphics.getFont().getSize());
        } else {
            gameOver(graphics);
        }
    }

    public void move() {
        for(int i = bodyParts; i>0; i--){
            x[i] = x[i-1];
            y[i] = y[i-1];
        }

        switch (direction){
            case 'U':
                y[0] = y[0] - UNIT_SIZE;
                break;
            case 'D':
                y[0] = y[0] + UNIT_SIZE;
                break;
            case 'R':
                x[0] = x[0] + UNIT_SIZE;
                break;
            case 'L':
                x[0] = x[0] - UNIT_SIZE;
                break;
        }
    }

    public void newApple() {
        appleX = random.nextInt(SCREEN_WIDTH/UNIT_SIZE)*UNIT_SIZE;
        appleY = random.nextInt(SCREEN_HEIGHT/UNIT_SIZE)*UNIT_SIZE;
    }

    public void checkApple() {
        if((x[0] == appleX) && (y[0] == appleY)) {
            bodyParts++;
            applesEaten++;
            newApple();
        }
    }

    public void checkCollisions() {
        // check if head collids with body
        for(int i = bodyParts; i > 0; i--){
            if((x[0] == x[i]) && (y[0] == y[i])) {
                running = false;
                break;
            }
        }

        // check if head touches left, right border
        if(x[0] < 0 || x[0] > SCREEN_WIDTH) {
            running = false;
        }
        // check if head touches top, bottom border
        if(y[0] < 0 || y[0] > SCREEN_HEIGHT){
            running = false;
        }

        if(!running){
            timer.stop();
        }
    }

    public void gameOver(Graphics graphics){
        // Score
        graphics.setColor(Color.red);
        graphics.setFont(new Font("Ink Free", Font.BOLD, 40));
        FontMetrics metrics = getFontMetrics(graphics.getFont());
        graphics.drawString("Score : " + applesEaten,
                (SCREEN_WIDTH - metrics.stringWidth("Score : " + applesEaten))/2,
                graphics.getFont().getSize());
        // Game Over Text
        //graphics.setColor(Color.red);
        graphics.setFont(new Font("Ink Free", Font.BOLD, 75));
        FontMetrics metrics2 = getFontMetrics(graphics.getFont());
        graphics.drawString("Game Over",
                (SCREEN_WIDTH - metrics2.stringWidth("Game Over"))/2,
                SCREEN_HEIGHT/2);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if(running){
            move();
            checkApple();
            checkCollisions();
        }
        repaint();
    }

    public class MyKeyAdapter extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e){
            switch (e.getKeyCode()) {
                case KeyEvent.VK_LEFT:
                    if(direction != 'R'){
                        direction = 'L';
                    }
                    break;
                case KeyEvent.VK_RIGHT:
                    if(direction != 'L') {
                        direction = 'R';
                    }
                    break;
                case KeyEvent.VK_UP:
                    if(direction != 'D') {
                        direction = 'U';
                    }
                    break;
                case KeyEvent.VK_DOWN:
                    if(direction != 'U') {
                        direction = 'D';
                    }
                    break;
            }
        }
    }
}

참고로 Apple은 Snake가 먹는 먹이이며, 먹으면 뱀의 길이가 1 증가한다. 

 

 

 

 

  • 참고

 

'JAVA' 카테고리의 다른 글

[Java 8] Optional  (0) 2022.10.07
[JAVA 8] Stream  (0) 2022.10.06
[JAVA 8] 인터페이스의 변화  (0) 2022.10.06
[JAVA 8] 함수형 인터페이스와 람다 표현식  (0) 2022.09.23
ListIterator  (0) 2022.09.23

+ Recent posts