지구정복
[JAVA] 11/18 | 다형성, 이클립스 자동완성기능 사용, 중첩클래스와 중첩인터페이스, 익명구현객체, 예외와 예외클래스 본문
[JAVA] 11/18 | 다형성, 이클립스 자동완성기능 사용, 중첩클래스와 중첩인터페이스, 익명구현객체, 예외와 예외클래스
nooh._.jl 2020. 11. 18. 17:41복습
상속에 있어서 부모
1. 일반 클래스
내용이 다 채워진 메서드
메서드() { }
2. 추상 클래스
메서드() { }
abstract 메서드();
*extends 오버라이드
3. interface
abstract 메서드();
*implements 오버라이드
다형성
자동형변환 - 작은자료형에서 큰 자료형할 때 자동적으로 된다.
강제형변환 - 큰 자료형에서 작은 자료형으로 바꿀 때 강제로 해야 한다.
*객체의 형변환은 상속이 전제조건
자동형변환 - 자식 -> 부모로 갈 때
강제형변환 - 부모 -> 자식으로 갈 때
OOP
캡슐
상속
추상
다형
ㅇ다형성
다형성은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 코드 측면에서 다형성은 하나의 타입에 여러 객체를 대입함으로써 다양한 기능을 이용할 수 있도록 해준다.
다형성을 위해 자바는 부모 클래스로 타입 변환을 허용한다.
즉, 부모 타입에 모든 자식 객체가 대입될 수 있다. 이것을 이용하면 객체는 부품화가 가능하다.
타입변환은 자동형변환과 강제형변환이 있다. 또한 상속이 전제가 되어야 한다.
자동형변환은 부모타입인 객체에 모든 자식타입의 객체가 대입될 수 있음을 의미하고 자바가 자동적으로 변환시킨다.
자식을 통해서 부모를 만드는 것이다.
Parent p1 = new Child(); //자동형변환
강제형변환은 자식타입으로 자동형변환된 부모타입객체를 자식타입객체에 대입하는 것을 의미한다.
Child c1 = (Child) p1; //강제형변환
하지만 아래와 같이 자식타입으로 형변환되지 않은 부모타입객체는 자식타입객체에 대입할 수 없다.
Parent p2 = new Parent();
Child c2 = (Child)p2; //에러
다음으로 상속과 자동타입변환에 대한 예제를 살펴보자.
class A {}
class B extends A {}
class C extends A {}
class D extends B {}
class E extends C {}
public class Ex1118_PromotionEx {
public static void main(String[] args) {
B b = new B();
C c = new C();
D d = new D();
E e = new E();
//자동형변환
A a1 = b;
A a2 = c;
A a3 = d;
A a4 = e;
B b1 = d;
C c1 = e;
//상속되지 않았으므로 자동형변환 불가
//B b3 = e;
//C c2 = d;
}
}
부모타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로만 한정된다.
그러나 예외가 있는데 부모메소드가 자식 클래스에서 오버라이딩되었다면 자식 클래스의 메소드가 대신 호출된다.
아래 예제를 살펴보자.
class Parent {
public void method1() {
System.out.println("Parent-method1()");
}
public void method2() {
System.out.println("Parent-method2()");
}
}
class Child extends Parent {
@Override
public void method2() {
System.out.println("Child-method2()");
}
public void method3() {
System.out.println("Child-method3()");
}
}
public class Ex1118_ChildEx {
public static void main(String[] args) {
Child child = new Child();
//자동타입변환
Parent parent = child;
parent.method1();
parent.method2(); //자식클래스에서 재정의된 메소드가 호출
//parent.method3(); //호출불가능, 왜냐하면 parent변수는 자식객체를 참조하지만 부모클래스의 멤버만 사용가능
}
}
이번에는 String클래스의 toString() 메서드를 재정의하고
배열을 이용해 부모타입인 objs 변수를 Car타입으로 자동형변환시킨 뒤
다시 objs를 Car타입으로 강제형변환해서 출력해보자.
class Car {
private String name;
private int numberOfWheels;
private String color;
Car(String name, int numberOfWheels, String color) {
this.name = name;
this.numberOfWheels = numberOfWheels;
this.color = color;
}
@Override
public String toString() {
return name + ":" + numberOfWheels + ":" + color;
}
}
public class Ex1118_Poly03 {
public static void main(String[] args) {
Car[] cars = new Car[3];
Car car1 = new Car("test1", 3, "Green");
Car car2 = new Car("test2", 4, "White");
Car car3 = new Car("test3", 5, "Black");
cars[0] = car1;
cars[1] = car2;
cars[2] = car3;
for(Car car : cars) {
System.out.println(car);
}
Object[] objs = new Car[3];
objs[0] = car1;
objs[1] = car2;
objs[2] = car3;
for(Object obj : objs) {
Car car = (Car)obj;
System.out.println(car);
}
}
}
이번에는 부모클래스의 추상메소드를 자식클래스들이 메소드재정의하고 다형성을(자동형변환) 이용해서메인클래스에서 재정의된 메소드를 호출하는 방법을 알아보자.
abstract class Employee {
abstract void salary();
}
class ChildEmployee1 extends Employee {
void salary() {
System.out.println("임원 Salary()");
}
}
class ChildEmployee2 extends Employee {
void salary() {
System.out.println("정직원 Salary()");
}
}
public class Ex1118_Poly04 {
public static void main(String[] args) {
//기본호출방법
ChildEmployee1 ce1 = new ChildEmployee1();
ce1.salary();
ChildEmployee2 ce2 = new ChildEmployee2();
ce2.salary();
//다형성을 이용한 호출방법
Employee e1 = new ChildEmployee1();
Employee e2 = new ChildEmployee2();
e1.salary();
e2.salary();
}
}
또 다른 예제를 살펴보자.
class Tire {
void run() {
System.out.println("일반 타이어");
}
}
class SnowTire extends Tire {
@Override
void run() {
System.out.println("스노우 타이어");
}
}
public class Ex1118_SnowTire01 {
public static void main(String[] args) {
SnowTire st = new SnowTire();
Tire t = st;
st.run(); //자식객체의 run() 호출
t.run(); //t는 자식타입으로 자동형변환되었으므로 재정의된 run() 호출
}
}
다음으로 자동형변환된 부모타입 변수가 어떤 타입인지 알아보자.
instanceof 명령어 실행
interface Vehicle { }
class Bus implements Vehicle { }
class Taxi { }
public class Ex1118_Poly05 {
public static void main(String[] args) {
Vehicle v = new Bus();
Taxi t = new Taxi();
System.out.println(v instanceof Bus);
System.out.println(v instanceof Object);
System.out.println(v instanceof Vehicle);
System.out.println(t instanceof Vehicle);
}
}
ㅇ이클립스 사용
-편집기에서 오버라이드나 getter, setter, 생성자 생성 등을 자동으로 해보자.
편집기 화면에서 오른쪽클릭 - source(또는 alt+shift+s) - Generate Getters and Setters - 만들고 싶은 필드 클릭
참고로 이클립스의 자동완성 단축키는 ctrl+spacebar 이다.
자동으로 getter과 setter이 만들어진다.
-이번에는 프로젝트를 임포트해보자.
먼저 해당 프로젝트를 지운다.
패키지 목록에서 오른쪽버튼 - import - General - Existing Projects into Workspace - 워크스페이션선택 - 다시생성
-이번에는 자동으로 상속받기 및 오버라이드를 해보자.
아래와 같이 자바프로젝트 src폴더 아래 패키지를 만들고 Parent1를 만든다.
Child클래스를 만들 때 아래 그림처럼 부모클래스를 정해준다.
그리고 alt+shift+s 를 눌러서 override/implements method 를 누른다음 아래 그림처럼 오버라이드할 메서드를 클릭한다.
-이번에는 추상클래스를 만들어보자. 아래 처럼 abstract를 체크하고 만든다.
Child2를 만들고 Parent2 를 상속받은 뒤 클래스를 만든다.
그러면 아래처럼 자동적으로 부모인 추상클래스의 추상메소드를 재정의하라고 나온다.
-이번에는 인터페이스를 만든다. 아래 그림처럼 인터페이스를 만든다.
다음 Child3클래스를 만들 때 interface 부분의 add를 누른다음 Parent3를 찾은 다음 클래스를 만든다.
그러면 자동적으로 인터페이스의 메소드를 재정의하라고 나온다.
-인터페이스 상속
인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와는 달리 다중 상속을 허용한다.
하위 인터페이스를 구현하는 클래스는 하위 인터페이스의 메소드뿐만 아니라 상위 인터페이스의 모든 추상 메소드에 대한 실체 메소드를 가지고 있어야 한다.
그렇기 때문에 구현클래스(Implemetation클래스) 로부터 객체를 생성하고 나서 아래와 같이 하위 및 상위 인터페이스 타입으로 변환이 가능하다.
하위 인터페이스로 타입 변환이 되면 상 하위 인터페이스에 선언된 모든 메소드를 사용할 수 있으나, 상위 인터페이스로 타입 변환되면 상위 인터페이스에 선언된 메소드만 사용가능하고 하위 인터페이스에 선언된 메소드는 사용할 수 없다.
package com.exam01;
public class Example {
public static void main(String[] args) {
//구현클래스로부터 구현객체를 만든다.
ImplementationC impl = new ImplementationC();
//구현객체 impl을 InterfaceA 타입으로 자동형변환시킨다.
//ia는 InterfaceA의 메소드만 호출할 수 있다.
InterfaceA ia = impl;
ia.methodA();
System.out.println();
//구현객체 impl을 InterfaceB 타입으로 자동형변환시킨다.
//ib는 InterfaceB의 메소드만 호출할 수 있다.
InterfaceB ib = impl;
ib.methodB();
System.out.println();
//구현객체 impl을 InterfaceC 타입으로 자동형변환시킨다.
//ic는 InterfaceA, B, c의 메소드 모두 호출할 수 있다.
InterfaceC ic = impl;
ic.methodA();
ic.methodB();
ic.methodC();
}
}
ㅁ중첩클래스와 중첩인터페이스
어떤 클래스는 여러 클래스와 관계를 맺지만 어떤 클래스는 특정 클래스와 관계를 맺는다. 클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나, 특정 클래스와 관계를 맺을 경우에는 관계 클래스를 클래스 내부에 선언하는 것이 좋다. 이때 중첩클래스(Nested Class)를 사용한다.
중첩클래스를 사용하면 두 클래스의 멤버들을 서로 쉽게 접근할 수 있고, 외부에는 불필요한 관계 클래스를 감춤으로써
코드의 복잡성을 줄일 수 있다.
중첩클래스는 선언되는 위치에 따라서 두 가지로 분류된다.
클래스의 멤버로서 선언되는 중첩 클래스를 멤버 클래스,
메소드 내부에서 선언되는 중첩 클래스를 로컬 클래스라고 한다.
멤버클래스는 클래스나 객체가 사용중이라면 언제든지 재상용이 가능하지만,
포컬 클래스는 메소드 실행 시에만 사용되고, 메소드가 실행 종료되면 없어진다.
중첩이란 클래스 안에 클래스가 또 선언되거나 클래스 안에서 인터페이스가 선언되는 것을 말한다.
이를 사용하는 이유는 해당 클래스 안에서만 중첩클래스나 중첩인터페이스를 사용한다는 의미이다.
멤버클래스의 바이트 코드 파일은 아래와 같다.
바깥클래스$멤버클래스.class
선언 위치에 따른 분류 | 선언 위치 | 설명 | |
멤버클래스 | 인스턴스 멤버 클래스 |
class A { class B { ... } } |
A객체를 생성해야만 사용할 수 있는 B 중첩 클래스 |
정적 멤버 클래스 | class A { static class B { ... } } |
A 클래스로 바로 접근할 수 있는 B 중첩 클래스 | |
로컬 클래스 | class A { void method() { class B { ... } } } |
method()가 실행할 때만 사용할 수 있는 B 중첩클래스 |
-인스턴스 멤버 클래스 만들기
class Outer {
//멤버필드
private int x1 = 100;
public int x2 = 100;
Outer() {
System.out.println("Outer 생성자");
}
//인스턴스 멤버 중첩 클래스
class Inner {
private int y1 = 200;
public int y2 = 200;
Inner() {
System.out.println("Inner 생성자");
}
public void viewInner() {
System.out.println(x1);
System.out.println(x2);
System.out.println(y1);
System.out.println(y2);
}
}
}
public class Ex1118_NestedEx01 {
public static void main(String[] args) {
Outer o = new Outer();
Outer.Inner oi = o.new Inner();
//바로 o.x1을 호출하면 접근제한때문에 오류가 난다.
//System.out.println(o.x1);
System.out.println(o.x2);
//줃첩클래스인 oi객체의 viewInner()을 이용하면 private접근제한이 걸린 멤버필드를 읽을 수 있다.
oi.viewInner();
}
}
-정적 멤버 클래스 만들기
정적멤버클래스는 외부클래스 객체를 만들 필요없이 바로 외부클래스.중첩클래스로 객체를 만들 수 있다.
이때 x1과 x2는 사용할 수 없다.
class Outer {
//멤버필드
private int x1 = 100;
public int x2 = 100;
Outer() {
System.out.println("Outer 생성자");
}
//인스턴스 멤버 중첩 클래스
static class Inner {
private int y1 = 200;
public int y2 = 200;
Inner() {
System.out.println("Inner 생성자");
}
public void viewInner() {
//System.out.println(x1);
//System.out.println(x2);
System.out.println(y1);
System.out.println(y2);
}
}
}
public class Ex1118_NestedEx02 {
public static void main(String[] args) {
//Outer o = new Outer();
Outer.Inner oi = new Outer.Inner();
oi.viewInner();
}
}
-로컬 클래스 만들기
로컬 클래스는 외부 클래스에 메서드를 선언하고 그 메서드 안에 중첩클래스를 선언한다.
로컬 클래스는 메소드가 실행될 때 메소드 내에서 객체를 생성하고 사용해야 한다.
class Outer {
void viewInner() {
//viewInner를 호출하면 Inner 클래스를 선언
class Inner {
void view() {
System.out.println("view 호출");
}
}
//생성과 실행
Inner i = new Inner();
i.view();
}
}
public class Ex1118_NestedEx03 {
public static void main(String[] args) {
Outer o = new Outer();
o.viewInner();
}
}
이때 폴더를 확인해보면 로컬클래스의 경우 아래와 같은 바이트 코드 파일로 생성된다.
바깥클래스$1로컬클래스.class
ㅇ익명 객체 만들기
인터페이스를 만들면 원래 구현클래스를 만들어서 추상메소드를 재정의해줘야하는데
실행 클래스에서 바로 인터페이스 타입의 객체를 만들고 중괄호 안에서 추상메소드를 재정의하면 바로 사용할 수 있다.
이를 익명 구현 객체라고 한다.
또한 익명 구현 객체에서 지역변수의 사용도 가능하다.
interface Inner {
int x = 10;
void viewInner();
}
//원래는 클래스를 만들어서 구현해야 한다.
//하지만 위 인터페이스 그 자체를 생성할 수 있다.
//이를 익명 객체라고 한다.
public class Ex1118_NestedEx04 {
public static void main(String[] args) {
int y = 100;
//익명 inner class
Inner i = new Inner() {
public void viewInner() {
System.out.println( x );
System.out.println( y ); //익명객체에서 지역변수 접근도 허용한다.
}
};
i.viewInner();
}
}
-익명 객체의 로컬 변수 사용
익명객체 내부에서는 바깥 클래스의 필드나 메소드는 제한 없이 사용할 수 있다. 문제는 메소드의 매개변수나 로컬 변수를 익명 객체에서 사용할 때이다. 메소드 내에서 생성된 익명 객체는 메소드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다. 매개변수나 로컬변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 되므로 문제가 발생한다.
익명 객체 내부에서 메소드의 매개변수나 로컬 변수를 사용할 경우, 이 변수들은 final 특성을 가져야 한다.
final 키워드는 자동으로 생성된다.
따라서 익명 객체에서 사용된 매개변수와 로컬변수는 모두 final 특성을 갖는다.
다음 예제는 매개변수와 로컬변수가 익명 객체에서 사용할 때 final특성을 갖고 있음을 보여준다.
interface Calculatable {
public int sum();
}
class Anonymous {
private int field;
public void method(final int arg1, int arg2) {
final int var1 = 0;
int var2 = 0;
field = 10;
Calculatable calc = new Calculatable() {
@Override
public int sum() {
int result = field + arg1 + arg2 + var1 + var2;
return result;
}
};
System.out.println(calc.sum());
}
}
public class Ex1118_Calculatable {
public static void main(String[] args) {
Anonymous a = new Anonymous();
a.method(0, 0);
}
}
ㅁ예외와 예외 클래스
예외란 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류를 말한다.
예외가 발생되면 프로그램은 곧바로 종료된다는 점에서는 에러와 동일하다.
그러나 예외는 예외처리를 통해 프로그램을 종료하지 않고 정상 실행 상태가 유지되도록 한다.
예외는 두 가지가 있다.
일반 예외는 컴파일러 체크 예외라고도 하는데, 자바 소스를 컴파일하는 과정에서 예외 처리 코드가필요한 지 검사하기 때문이다. 만약 예외 처리 코드가 없다면 컴파일 오휴가 발생한다.
실행 예외는 컴파일하는 과정에서 예외 처리 코드를 검사하지 않는 예외를 말한다.
컴파일 시 예외 처리를 확인하는 차이일 뿐, 두 가지 예외는 모두 예외 처리가 필요하다.
자바에서는 예외를 클래스로 관리한다. JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성한다. 그리고 나서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해준다.
모든 예외 클래스들은 java.lang.Exception 클래스를 상속받는다.
-에러
컴파일에러
java -> class 변환 시 발생
이는 문법오류 - 컴파일러가 확인해준다.
대표적인오류: 오타
런타임에러
class 실행 시 발생
잘못된 값 사용 또는 값 선언으로 인해 오류
=> if문으로 에러를 처리할 수 있다.
=> Exception 으로 에러를 처리할 수 있다.(고급에러처리기법) => Exception 클래스가 있다.
Exception은 런타임에러만 처리할 수 있다.
런타임에러의 대표적인 예로 0으로 나누기하는 상황이다.
컴파일은 정상적으로 되지만 실행시 java.lang.ArithmeticException 에러가 난다.
public class Ex1118_Exception01 {
public static void main(String[] args) {
System.out.println("시작");
//int num1 = 2;
int num1 = 0;
int num2 = 10;
int result = num2/num1;
System.out.println(result);
System.out.println("종료");
}
}
위의 에러를 if문으로 처리해보자.
public class Ex1118_Exception01 {
public static void main(String[] args) {
System.out.println("시작");
//int num1 = 2;
int num1 = 0;
int num2 = 10;
if(num1 == 0) {
System.out.println("0으로 나눌 수 없습니다.");
} else {
int result = num2/num1;
System.out.println(result);
}
System.out.println("종료");
}
}
이제 try catch문으로 에러를 처리해보자.
이때 ArithmeticException e는 익셉션 객체이다. 에러가 발생하면 JVM이 내부에서 자동적으로
ArithmeticException e = new ArithmeticException(); 를 실행해서 익셉션 객체를 만든다.
익셉션 객체 e를 통해서 에러 메세지를 보려면 e.getMessage() 를 통해서 볼 수 있다.
또한 finaly {} (후처리조건) 를 통해서 에러가 발생하든 안하든 무조건 실행시킬 수 있다.
public class Ex1118_Exception03 {
public static void main(String[] args) {
System.out.println("시작");
int num1 = 0;
int num2 = 10;
try {
int result = num2/num1;
System.out.println(result);
} catch(ArithmeticException e) {
//ArithmeticException e = new ArithmeticException();
System.out.println("익셉션 발생"+ e.getMessage());
} finally {
System.out.println("에러 발생하든 안하든 무조건 실행하는 구문");
}
System.out.println("종료");
}
}
이번에는 문자열 변수에 null일경우 java.lang.NullPointerException 에러가 나는 경우를 확인하자.
public class Ex1118_Exception02 {
public static void main(String[] args) {
System.out.println("시작");
String data = null;
System.out.println(data.toString());
System.out.println("종료");
}
}
if문으로 아래처럼 에러처리한다.
public class Ex1118_Exception02 {
public static void main(String[] args) {
System.out.println("시작");
String data = null;
if (data ==null) {
System.out.println("객체 사용 불가");
} else {
System.out.println(data.toString());
}
System.out.println("종료");
}
}
존재하지 않는 클래스로 객체를 만들어서
ClassNotfoundException 에러를 발생시키자. 이때 이 에러는 일반예외이므로 컴파일러는 개발자로 하여금 예외 처리 코드를 작성하도록 요구한다.
public class Ex1118_TryCatchFinallyExample {
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.longString2");
} catch(ClassNOtFoundException e) {
System.out.println("클래스가 존재하지 않습니다.");
}
}
}
하지만 ArrayIndexOutOfBoundsException이나, NumberFormatException과 같은 실행 예외는 컴파일러가 예외 처리 코드를 체크하지 않기 때문에 개발자의 경험에 의해서 에러처리를 해줘야한다.
아래 코드에서 첫 번째 try catch문 catch 블럭에서 return을 사용하는 이유는 그 밑에 코드를 실행하지 않고 프로그램을
종료시키는 기능이다. 만약 return을 지우면 아래 try catch문까지 출력된다.
public class Ex1118_TryCatchFinallyRuntimeExceptionExample {
public static void main(String[] args) {
String data1 = null;
String data2 = null;
try {
data1 = args[0];
data2 = args[1];
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("실행 매개값의 수가 부족합니다.");
System.out.println("[실행 방법]");
System.out.println("java TryCatchFinallyRuntimeExceptionExample num1 num2");
return;
}
try {
int v1 = Integer.parseInt(data1);
int v2 = Integer.parseInt(data2);
int result = v1 + v2;
System.out.println(data1 + "+" + data2 + "=" + result);
} catch(NumberFormatException e) {
System.out.println("숫자로 변환할 수 없습니다.");
} finally {
System.out.println("다시 실행하세요.");
}
}
}
args값을 주지 않으면 아래와 같은 결과가 나온다.
이제 args값으로 숫자가 아닌 문자를 주면 아래와 같은 결과가 출력된다.
이제 제대로 숫자값을 주면 아래와 같은 결과가 나온다.
이때 "다시 실행하세요"는 finally구문이기때문에 무조건 실행된다.
return;을 try문에 넣어도 finally구문은 무조선 실행된다.
try {
int v1 = Integer.parseInt(data1);
int v2 = Integer.parseInt(data2);
int result = v1 + v2;
System.out.println(data1 + "+" + data2 + "=" + result);
return; //리턴추가
} catch(NumberFormatException e) {
System.out.println("숫자로 변환할 수 없습니다.");
} finally {
System.out.println("다시 실행하세요.");
}