package org.opentutorials.javatutorials.object;
class Calculator{
int left, right;
public void setOprands(int left, int right){
this.left = left;
this.right = right;
}
public void sum(){
System.out.println(this.left+this.right);
}
public void avg(){
System.out.println((this.left+this.right)/2);
}
}
public class CalculatorDemo4 {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.setOprands(10, 20);
c1.sum();
c1.avg();
Calculator c2 = new Calculator();
c2.setOprands(20, 40);
c2.sum();
c2.avg();
}
}
위의 코드를 보면 main 메소드가 있는 CalculatorDemo4 라는 클래스 외에, Calculator라는 새로운 클래스가 있다. 클래스는 연관되어 있는 변수와 메소드의 집합이라고 말할 수 있다. Calculator 클래스에는 계산과 관련되어있는 left, right 변수와 sum, avg 메소드가 포함되어있다.
클래스는 인스턴스에 의해서 구체적으로 실현되고, 사용될 수 있다.
Calculator c1 = new Calculator();
여기에서 c1을 인스턴스라고 할 수 있다. c1 앞에 적힌 Calculator는 우리가 정의한 클래스의 이름인데, 마치 하나의 자료형과 같이 사용된 것을 볼 수 있다. 즉, 클래스를 정의하는 것은 사용자 정의 데이터 타입을 만드는 것이라고 바꾸어 말할 수 있다.
c1이라는 인스턴스를 생성하였으면, 이제 우리는 Calculator 클래스에서 정의한 메소드들을 사용할 수 있다.
package org.opentutorials.javatutorials.classninstance;
class Calculator {
static double PI = 3.14;
int left, right;
public void setOprands(int left, int right) {
this.left = left;
this.right = right;
}
public void sum() {
System.out.println(this.left + this.right);
}
public void avg() {
System.out.println((this.left + this.right) / 2);
}
}
public class CalculatorDemo1 {
public static void main(String[] args) {
Calculator c1 = new Calculator();
System.out.println(c1.PI);
Calculator c2 = new Calculator();
System.out.println(c2.PI);
System.out.println(Calculator.PI);
}
}
Calculator 클래스를 보면 static double PI = 3.14; 가 있다. static을 맴버 앞에 붙이면 클래스의 맴버가 된다. 클래스 변수에 접근하는 방법을 알아보자.
System.out.println(c1.PI);
System.out.println(Calculator.PI);
첫 번째 방법의 경우 인스턴스를 통해서 클래스 맴버인 PI에 접근한다.
두 번째 방법은 클래스를 직접 통해서 PI에 접근한다. 이 경우에는 PI에 접근하기 위해서 따로 인스턴스를 생성할 필요가 없다.
package org.opentutorials.javatutorials.classninstance;
class Calculator3{
public static void sum(int left, int right){
System.out.println(left+right);
}
public static void avg(int left, int right){
System.out.println((left+right)/2);
}
}
public class CalculatorDemo3 {
public static void main(String[] args) {
Calculator3.sum(10, 20);
Calculator3.avg(10, 20);
Calculator3.sum(20, 40);
Calculator3.avg(20, 40);
}
}
Calculator3 클래스를 보면 메소드를 정의할 때 static을 붙이고 있다. 이 경우 해당 메소드는 클래스 메소드가 되며, 인스턴스를 만들 필요 없이 바로 클래스에 접근해서 해당 메소드를 사용할 수 있다.
package org.opentutorials.javatutorials.scope;
public class ScopeDemo {
static void a() {
int i = 0;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
a();
System.out.println(i);
}
}
}
이 코드를 보면 a 메소드에 의해 i의 값이 계속 0으로 초기화되기 때문에 반복문이 무한 루프에 빠질 것으로 예상할 수 있다. 하지만 유효 범위라는 개념 덕분에 이 코드를 실행시키면 0,1,2,3,4가 정상적으로 출력된다.
a라는 메소드 내부에서 i가 선언됐기 때문에, 이 변수의 값을 어떻게 바꿔도 메소드 외부에는 영향을 주지 않는다.
package org.opentutorials.javatutorials.scope;
public class ScopeDemo2 {
static int i;
static void a() {
i = 0;
}
public static void main(String[] args) {
for (i = 0; i < 5; i++) {
a();
System.out.println(i);
}
}
}
반면 위와 같이 코드를 작성한다면, 무한 루프에 빠지게 될 것이다. i가 어떤 메소드 안에서도 선언되지 않았고 클래스 소속의 변수가 되었기 때문에 모든 메소드에서 접근할 수 있게 된다.
위 코드에서 메소드 a를 선언하는 부분을 아래와 같이 변경하면 에러가 발생하지 않을 것이다.
static void a() {
int i = 0;
}
int가 추가되었다. 이미 클래스 소속의 변수 i가 있는데, 메소드 내부에서 i가 다시 한 번 선언되었다. 이 때는 메소드 내부의 변수가 우선시 되어서 새로운 i가 생성된다. (그리고 메소드가 끝나면 삭제된다.)
즉, 지역적인 것이 전역적인 것보다 우선 순위가 높다고 말할 수 있다. 클래스 전역에서 접근 가능한 변수를 전역변수, 메소드 내에서만 접근할 수 있는 변수를 지역변수라고 한다.
package org.opentutorials.javatutorials.scope;
public class ScopeDemo6 {
static int i = 5;
static void a() {
int i = 10;
b();
}
static void b() {
System.out.println(i);
}
public static void main(String[] args) {
a();
}
}
위 코드에서 전역변수 i는 5의 값을 가지고, 메소드 a 내부에서 선언된 지역변수 i는 10의 값을 가진다. 프로그램이 실행되면 메소드 b가 메소드 a 내부에서 i를 출력하게 되어있는데, 이 때 메소드 b는 지역변수를 사용할까 전역변수를 사용할까?
전역변수 5를 출력하게 된다. 어쨌든 결과적으로 메소드 b 내부에 i라고 하는 변수가 정의되어있지 않기 때문에 클래스에 소속된 전역변수 i를 사용하는 것이다. 이러한 방식을 정적 스코프 혹은 렉시컬 스코프라고 부르고, 자바를 포함한 대부분의 현대 언어는 정적 스코프를 채택하고 있다.
Calculator c1 = new Calculator();
c1.setOprands(10, 20);
c1.sum();
c1.avg();
이전에 사용했던 계산기 예제를 사용하려면 위와 같이 작성해야한다. 이 때,
c1.setOprands(10,20); 를 쓰는 것을 까먹으면 sum, avg 메소드 사용이 불가능할 것이다. 어짜피 필수적으로 사용해야 하는 setOprands 같은 메소드를 인스턴스를 생성할 때 강제적으로 사용하게 만드는 방법이 있다. 생성자를 사용하면 위의 코드를 아래와 같이 사용하도록 변경할 수 있다.
Calculator c1 = new Calculator(10, 20);
c1.sum();
c1.avg();
이렇게 사용하기 위해서는 코드를 아래와 같이 작성해야 한다.
package org.opentutorials.javatutorials.constructor;
class Calculator {
int left, right;
public Calculator(int left, int right) {
this.left = left;
this.right = right;
}
public void sum() {
System.out.println(this.left + this.right);
}
public void avg() {
System.out.println((this.left + this.right) / 2);
}
}
생성자는 기본적으로 메소드의 한 종류라고 봐도 될 것 같다. 생성자는 이름과 같이 그 객체를 생성할 때 호출된다.
package org.opentutorials.javatutorials.Inheritance.example1;
class Calculator {
int left, right;
public void setOprands(int left, int right) {
this.left = left;
this.right = right;
}
public void sum() {
System.out.println(this.left + this.right);
}
public void avg() {
System.out.println((this.left + this.right) / 2);
}
}
class SubstractionableCalculator extends Calculator {
public void substract() {
System.out.println(this.left - this.right);
}
}
public class CalculatorDemo1 {
public static void main(String[] args) {
SubstractionableCalculator c1 = new SubstractionableCalculator();
c1.setOprands(10, 20);
c1.sum();
c1.avg();
c1.substract();
}
}
Calculator 클래스의 내용을 수정하지 않고 빼기 기능을 넣는 방법이 있을까? 상속을 이용하면 가능하다.
SubstractionableCalculator 클래스를 새로 생성하고, 이 클래스가 Calculator를 상속하도록 지정하면 Calculator 내의 메소드를 따로 정의하지 않아도 상속받은 클래스 내에서 자유롭게 사용할 수 있다.
java에서 하위 클래스가 호출될 때 자동으로 상위 클래스의 기본 생성자를 호출하게 된다. 그런데, 상위 클래스에 매개변수가 있는 생성자가 있다면 자바는 자동으로 상위 클래스의 기본 생성자를 만들어주지 않는다. 이를 해결하기 위해서는 상위 클래스에 기본 생성자를 직접 입력해주면 된다.
하지만, 기본 생성자를 직접 입력해주지 말고, 상위 클래스에서 사용 중인 매개변수가 있는 생성자를 하위 클래스에서 직접 호출할 수도 있다. 이 때 사용하는 것이 super이다. super는 상위 클래스를 가리키는 키워드이다.
package org.opentutorials.javatutorials.Inheritance.example3;
class Calculator {
int left, right;
public Calculator(){}
public Calculator(int left, int right){
this.left = left;
this.right = right;
}
public void setOprands(int left, int right) {
this.left = left;
this.right = right;
}
public void sum() {
System.out.println(this.left + this.right);
}
public void avg() {
System.out.println((this.left + this.right) / 2);
}
}
class SubstractionableCalculator extends Calculator {
public SubstractionableCalculator(int left, int right) {
super(left, right);
}
public void substract() {
System.out.println(this.left - this.right);
}
}
public class CalculatorConstructorDemo5 {
public static void main(String[] args) {
SubstractionableCalculator c1 = new SubstractionableCalculator(10, 20);
c1.sum();
c1.avg();
c1.substract();
}
}
이렇게 하면 하위 클래스(SubstractionableCalculator)에서 상위 클래스의 생성자를 호출할 수 있다. 하위 클래스에서 super를 사용할 때 중요한 점은 super가 가장 먼저 나타나야 한다는 점이다.
package org.opentutorials.javatutorials.overriding.example1;
class Calculator {
int left, right;
public void setOprands(int left, int right) {
this.left = left;
this.right = right;
}
public void sum() {
System.out.println(this.left + this.right);
}
public void avg() {
System.out.println((this.left + this.right) / 2);
}
}
class SubstractionableCalculator extends Calculator {
public void sum() {
System.out.println("실행 결과는 " +(this.left + this.right)+"입니다.");
}
public void substract() {
System.out.println(this.left - this.right);
}
}
public class CalculatorDemo {
public static void main(String[] args) {
SubstractionableCalculator c1 = new SubstractionableCalculator();
c1.setOprands(10, 20);
c1.sum();
c1.avg();
c1.substract();
}
}
상위 클래스에서 sum 메소드를 정의하고 있는데, 이를 상속받은 하위 클래스에서 똑같은 sum 메소드를 정의했다. 이 때, c1.sum(); 으로 sum 메소드를 실행시키면 하위 클래스의 sum 메소드가 우선적으로 실행된다. 즉, 하위 클래스에서 상위 클래스와 동일한 메소드를 정의하면 상위 클래스에서 물려받은 기본 동작 방법을 변경하는 효과를 가지는 것이다. 이것을 메소드 오버라이딩이라고 한다.
위의 조건들이 같아야 오버라이딩이 가능하고, 위 조건들을 메소드의 서명이라고 한다.
package org.opentutorials.javatutorials.overloading.example1;
class Calculator{
int left, right;
int third = 0;
public void setOprands(int left, int right){
System.out.println("setOprands(int left, int right)");
this.left = left;
this.right = right;
}
public void setOprands(int left, int right, int third){
System.out.println("setOprands(int left, int right, int third)");
this.left = left;
this.right = right;
this.third = third;
}
public void sum(){
System.out.println(this.left+this.right+this.third);
}
public void avg(){
System.out.println((this.left+this.right+this.third)/3);
}
}
public class CalculatorDemo {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.setOprands(10, 20);
c1.sum();
c1.avg();
c1.setOprands(10, 20, 30);
c1.sum();
c1.avg();
}
}
이름은 같지만 시그니처는 다른 메소드를 중복으로 선언할 수 있는 방법을 메소드 오버로딩이라고 한다. 위의 예제를 보면 같은 setOprands 함수를 실행하더라도, 매개변수의 개수에 따라서 서로 다른 메소드를 호출하고 있다.
| public | protected | default | private | |
|---|---|---|---|---|
| 같은 패키지, 같은 클래스 | 허용 | 허용 | 허용 | 허용 |
| 같은 패키지, 상속 관계 | 허용 | 허용 | 허용 | 불용 |
| 같은 패키지, 상속 관계 아님 | 허용 | 허용 | 허용 | 불용 |
| 다른 패키지, 상속 관계 | 허용 | 허용 | 불용 | 불용 |
| 다른 패키지, 상속 관계 아님 | 허용 | 불용 | 불용 | 불용 |
클래스에 대한 접근 제어자
public 클래스가 포함된 소스코드는 public 클래스의 클래스명과 소스코드의 파일명이 같아야한다. 즉, 하나의 소스 코드에는 하나의 public 클래스만 존재할 수 있다.
abstract는 상속을 강제하는 것이다.
추상 클래스는 추상 메소드를 가질 수 있는데, 추상 메소드는 그 본체가 없고 시그니처만이 정의되어있는 메소드이다.
public abstract int test();
보다시피, 메소드임에도 불구하고 중괄호{ } 안에 구체적인 로직이 없고 시그니처만 정의되어있는 모습이다. abstract 수식어가 붙은 메소드에 본체를 작성하면 에러가 발생한다.
맴버 중에 하나라도 abstract 수식어를 가지고 있다면 해당 맴버를 소유하고 있는 클래스도 추상 클래스로 정의해야한다. 추상 클래스는 구체적인 메소드의 내용이 없기때문에 인스턴스화 해서 사용할 수 없다. 따라서 추상 클래스를 사용하려면 추상 클래스를 상속한 다른 클래스에서 추상 메소드를 오버라이딩 해서 사용해야 한다.
package org.opentutorials.javatutorials.interfaces.example1;
interface I{
public void z();
}
class A implements I{
public void z(){}
}
I라고 하는 인터페이스가 정의되었는데, 인터페이스 내부의 메소드인 z에 몸체가 없다. (마치 추상 메소드와 같다) 그리고 아래에 A라는 클래스가 인터페이스 I를 구현하고 있는데, 클래스 내부에서 메소드 z가 다시 정의되고 있다. (오버라이딩) 인터페이스에서 정의된 메소드를 구현하지 않으면 에러가 발생한다. 시그니처가 달라도 에러가 발생한다. 따라서 인터페이스는 일종의 '규제'라고 볼 수 있다.
인터페이스를 사용하는 이유는 무엇일까? 인터페이스는 협업을 할 때 도움이 되기 때문에 사용한다. A 부분을 맡은 개발자와 B 부분을 맡은 개발자가 동시에 프로그램을 작성하고 이후에 합치기로 합의했다. 각자 코드를 작성하고 합치려고 봤더니, 호환이 되지 않는 경우가 생길 수 있다. 이런 상황을 방지하기 위해서 미리 인터페이스를 만들어둔다. 그리고 그 인터페이스를 상속한 A와 B를 각자 개발하고 이후에 합친다면 호환이 되지 않을 가능성이 낮아지게 되는 것이다.
package org.opentutorials.javatutorials.exception;
class Calculator{
int left, right;
public void setOprands(int left, int right){
this.left = left;
this.right = right;
}
public void divide(){
System.out.print("계산결과는 ");
System.out.print(this.left/this.right);
System.out.print(" 입니다.");
}
}
public class CalculatorDemo {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.setOprands(10, 0);
c1.divide();
}
}
위의 코드는 에러가 발생한다. 왜냐하면 10을 0으로 나누려고 했기 때문이다. 이처럼 프로그램에서는 예상치 못한 오류가 발생하는 경우가 많다. 이럴 때 예외를 처리하는 방법이 있다. 아래와 같이 코드를 작성하면 된다.
package org.opentutorials.javatutorials.exception;
class Calculator{
int left, right;
public void setOprands(int left, int right){
this.left = left;
this.right = right;
}
public void divide(){
try {
System.out.print("계산결과는 ");
System.out.print(this.left/this.right);
System.out.print(" 입니다.");
} catch(Exception e){
System.out.println("오류가 발생했습니다 : "+e.getMessage());
}
}
}
public class CalculatorDemo {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.setOprands(10, 0);
c1.divide();
Calculator c2 = new Calculator();
c2.setOprands(10, 5);
c2.divide();
}
}
이 코드를 이해하기 위해 try ... catch 문을 알아보자.
try ... catch문은 아래와 같이 사용하면 된다.
try {
예외의 발생이 예상되는 로직
} catch (예외클래스 인스턴스) {
예외가 발생했을 때 실행되는 로직
}
} catch(Exception e){
System.out.println("오류가 발생했습니다 : "+e.getMessage());
}
이 코드에서 e는 변수다. 앞의 Exception은 변수의 데이터 타입이 Exception이라는 의미다. Exception은 자바에서 기본적으로 제공하는 클래스로 java.lang에 소속되어 있다. 예외가 발생하면 자바는 마치 메소드를 호출하듯이 catch를 호출하면서 그 인자로 Exception 클래스의 인스턴스를 전달하는 것이다.
e.getMessage()는 자바가 전달한 인스턴스의 메소드 중 getMessage를 호출하는 코드인데, getMessage는 오류의 원인을 사람이 이해하기 쉬운 형태로 리턴하도록 약속되어 있다.
try {
예외의 발생이 예상되는 로직
} catch (예외클래스 인스턴스) {
예외가 발생했을 때 실행되는 로직
} finally {
예외여부와 관계없이 실행되는 로직
}
finally문 내부의 로직은 예외 여부와 관계없이 항상 실행된다.
자바에서 모든 클래스는 사실 Object 클래스를 상속받고 있는 것이다. 모든 클래스가 공통으로 포함하고 있어야 하는 기능을 제공하기 위해서이다.