패키지
여러 개의 파일이 뒤죽박죽 섞여 있을 때 파일들을 성격에 맞게 분류하여 새 폴더에 저장하는 식으로 말이다. 자바의 패키지도 이와 동일한 개념이라 할 수 있다. 정리하면 패키지(package)는 비슷한 성격의 클래스들을 모아 놓은 자바의 디렉터리이다.
패키지 만들기
house 패키지에서 클래스를 생성하면 다음처럼 package house;와 같은 문장이 자동으로 삽입된다.
house/HouseKim.java
package house;
public class HouseKim {
}
house/HousePark.java
package house;
public class HousePark {
}
package는 이 파일이 어떤 패키지의 파일인지를 알려 주는 역할을 한다.
서브 패키지란?
서브 패키지는 기본 패키지 안에 존재하는 하위 패키지이다. 이를 사용해 기본 패키지 내의 클래스들을 분류하여 체계적으로 관리하고, 가독성을 향상시킬 수 있다.
house/person/EungYongPark.java
package house.person;
public class EungYongPark {
}
EungYongPark 클래스의 package가 house.person으로 생성되었다. 이렇게 패키지는 도트(.)를 이용하여 서브 패키지를 표시한다. 다시 말해, house.person은 house 패키지의 서브패키지이다.
패키지 사용하기
import house.HouseKim;
public class Sample {
public static void main(String[] args) {
HouseKim kim = new HouseKim();
}
}
또는 다음과 같이 * 기호를 이용해 house 패키지 내의 모든 클래스를 사용할 수 있다.
import house.*;
public class Sample {
public static void main(String[] args) {
HouseKim kim = new HouseKim();
HousePark park = new HousePark();
}
}
만약 HouseKim과 동일한 패키지 내에 있는 클래스(여기서는 HousePark)라면 HouseKim 클래스를 사용하기 위해서 따로 import할 필요는 없다. 같은 패키지 내에서는 import 없이도 사용할 수 있다.
package house;
public class HousePark {
public static void main(String[] args) {
HouseKim kim = new HouseKim(); // HouseKim 사용을 위해서 import가 필요없다.
}
}
패키지를 사용하는 이유
패키지를 사용하면 비슷한 성격의 클래스들끼리 묶을 수 있어 클래스의 분류가 용이하다. 그리고 자바 코드를 작성하다 보면 다른 사람이 작성한 자바 클래스나 라이브러리를 사용해야할 경우도 많이 생기는데, 이때 클래스명이 동일한 경우도 발생할 수 있을 것이다. 하지만 패키지명이 다르면 클래스명이 동일해도 충돌 없이 사용할 수 있다.
- 클래스의 분류가 용이하다(비슷한 것끼리 묶는다).
- 패키지가 다르다면 동일한 클래스명을 사용할 수 있다.
접근 제어자
접근 제어자(access modifier)를 사용하여 변수나 메서드의 사용 권한을 설정할 수 있다. 다음과 같은 접근 제어자를 사용하여 사용 권한을 설정할 수 있다.
- private
- default
- protected
- public
private
접근 제어자가 private으로 설정되었다면 private이 붙은 변수나 메서드는 해당 클래스 안에서만 접근이 가능하다.
public class Sample {
private String secret;
private String getSecret() {
return this.secret;
}
}
default
접근 제어자를 별도로 설정하지 않는다면 변수나 메서드는 default 접근 제어자가 자동으로 설정되어 동일한 패키지 안에서만 접근이 가능하다.
house/HouseKim.java
package house; // 패키지가 동일하다.
public class HouseKim {
String lastname = "kim"; // lastname은 default 접근제어자로 설정된다.
}
house/HousePark.java
package house; // 패키지가 동일하다.
public class HousePark {
String lastname = "park";
public static void main(String[] args) {
HouseKim kim = new HouseKim();
System.out.println(kim.lastname); // HouseKim 클래스의 lastname 변수를 사용할 수 있다.
}
}
kim
protected
접근 제어자가 protected로 설정되었다면 protected가 붙은 변수나 메서드는 동일 패키지의 클래스 또는 해당 클래스를 상속받은 클래스에서만 접근이 가능하다.
house/HousePark.java
package house; // 패키지가 서로 다르다.
public class HousePark {
protected String lastname = "park";
}
house/person/EungYongPark.java
package house.person; // 패키지가 서로 다르다.
import house.HousePark;
public class EungYongPark extends HousePark { // HousePark을 상속했다.
public static void main(String[] args) {
EungYongPark eyp = new EungYongPark();
System.out.println(eyp.lastname); // 상속한 클래스의 protected 변수는 접근이 가능하다.
}
}
park
public
접근 제어자가 public으로 설정되었다면 public 접근 제어자가 붙은 변수나 메서드는 어떤 클래스에서도 접근이 가능하다.
package house;
public class HousePark {
protected String lastname = "park";
public String info = "this is public message.";
}
HousePark의 info 변수는 public 접근 제어자가 붙어 있으므로 어떤 클래스라도 접근이 가능하다. 그래서 다음과 같이 작성할 수 있다.
import house.HousePark;
public class Sample {
public static void main(String[] args) {
HousePark housePark = new HousePark();
System.out.println(housePark.info);
}
}
this is public message.
스태틱
스태틱(static)은 클래스에서 공유되는 변수나 메서드를 정의할 때 사용된다. 이번 절에서는 스태틱에 대해서 자세히 알아보자.
static 변수
class HouseLee {
String lastname = "이";
}
public class Sample {
public static void main(String[] args) {
HouseLee lee1 = new HouseLee();
HouseLee lee2 = new HouseLee();
}
}
HouseLee 클래스를 만들고 객체를 생성하면 객체마다 객체 변수 lastname을 저장하기 위한 메모리가 별도로 할당된다.
하지만 가만히 생각해 보면 HouseLee 클래스의 lastname은 어떤 객체이든지 동일한 값인 ‘이’이어야 할 것 같지 않은가? 이렇게 항상 값이 변하지 않는다면 static을 사용해 메모리 낭비를 줄일 수 있다.
class HouseLee {
static String lastname = "이";
}
public class Sample {
public static void main(String[] args) {
HouseLee lee1 = new HouseLee();
HouseLee lee2 = new HouseLee();
}
}
lastname 변수에 static 키워드를 붙이면 자바는 메모리 할당을 딱 한 번만 하게 되어 메모리를 적게 사용할 수 있다.
static을 사용하는 또 다른 이유는 값을 공유할 수 있기 때문이다. static으로 설정하면 같은 메모리 주소만을 바라보기 때문에 static 변수의 값을 공유하게 되는 것이다.
class Counter {
int count = 0;
Counter() {
this.count++;
System.out.println(this.count);
}
}
public class Sample {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
}
}
1
1
객체 c1, c2를 생성할 때 생성자에서 객체 변수인 count의 값을 1씩 증가시키더라도 c1, c2와 count는 서로 다른 메모리를 가리키고 있기 때문에 원하던 결과(count가 증가된 결과)가 나오지 않는다. 객체 변수는 항상 독립적인 값을 갖기 때문에 당연한 결과이다.
class Counter {
static int count = 0;
Counter() {
count++; // count는 더이상 객체변수가 아니므로 this를 제거하는 것이 좋다.
System.out.println(count); // this 제거
}
}
public class Sample {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
}
}
1
2
static 메서드
static이라는 키워드가 메서드 앞에 붙으면 이 메서드는 스태틱 메서드(static method)가 된다.
class Counter {
static int count = 0;
Counter() {
count++;
System.out.println(count);
}
public static int getCount() {
return count;
}
}
public class Sample {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.getCount()); // 스태틱 메서드는 클래스를 이용하여 호출
}
}
1
2
2
Counter 클래스에 getCount()라는 스태틱 메서드를 추가했다. 메서드 앞에 static 키워드를 붙이면 Counter.getCount()와 같이 객체 생성 없이도 클래스를 통해 메서드를 직접 호출할 수 있다.
import java.text.SimpleDateFormat;
import java.util.Date;
class Util {
public static String getCurrentDate(String fmt) {
SimpleDateFormat sdf = new SimpleDateFormat(fmt);
return sdf.format(new Date());
}
}
public class Sample {
public static void main(String[] args) {
System.out.println(Util.getCurrentDate("yyyyMMdd")); // 오늘 날짜 출력
}
}
싱글톤 패턴
static에 대한 개념이 생겼기 때문에 싱글톤을 이해하는 것이 어렵지 않다. 싱글톤은 단 하나의 객체만을 생성하게 강제하는 디자인 패턴이다. 다시 말해, 클래스를 통해 생성할 수 있는 객체가 한 개만 되도록 만드는 것이 싱글톤이다.
class Singleton {
private Singleton() {
}
}
public class Sample {
public static void main(String[] args) {
Singleton singleton = new Singleton(); // 컴파일 오류가 발생한다.
}
}
이같이 코드를 작성하면 컴파일 오류가 발생한다. 왜냐하면 Singleton 클래스의 생성자에 private 접근 제어자를 설정하여 다른 클래스에서 Singleton 클래스의 생성자로의 접근을 막았기 때문이다. 이렇게 생성자를 private으로 만들어 버리면 Singleton 클래스를 다른 클래스에서 new를 이용하여 생성할 수 없게 된다.
class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return new Singleton(); // 같은 클래스이므로 생성자 호출이 가능하다.
}
}
public class Sample {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
}
}
이같이 getInstance라는 스태틱 메서드를 이용하여 Singleton 클래스의 객체를 생성할 수 있다. 하지만 getInstance를 호출할 때마다 새로운 객체가 생성되기 때문에 이 역시 싱글톤이 아니다.
class Singleton {
private static Singleton one;
private Singleton() {
}
public static Singleton getInstance() {
if(one==null) {
one = new Singleton();
}
return one;
}
}
public class Sample {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true 출력
}
}
true
Singleton 클래스에 one이라는 static 변수를 작성하고, getInstance 메서드에서 one값이 null인 경우에만 객체를 생성하도록 하여 one 객체가 딱 한 번만 만들어지도록 했다.
예외 처리
프로그램을 만들다 보면 수없이 많은 예외 상황이 발생한다. 물론 예외가 발생하는 것은 프로그램이 오동작을 하지 않게 하기 위한 자바의 배려이다. 하지만 이러한 예외 상황을 무시하고 싶을 때도 있고, 적절한 처리를 하고 싶을 때도 있다. 원하는 대로 예외를 처리하기 위해서 try ~ catch, throws 구문을 이용해 보자. 예외 처리 방법을 알게 되면 보다 안전하고 유연한 프로그래밍을 구사할 수 있다.
예외는 언제 발생하는가?
import java.io.*;
public class Sample {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("나없는파일"));
br.readLine();
br.close();
}
}
이 코드를 실행하면 존재하지 않는 파일을 열려고 시도했기 때문에 원하는 파일을 찾을 수 없다는 FileNotFoundException라는 예외가 발생한다.
Exception in thread "main" java.io.FileNotFoundException: 나없는파일 (지정된 파일을 찾을 수 없습니다)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(Unknown Source)
at java.io.FileInputStream.<init>(Unknown Source)
at java.io.FileReader.<init>(Unknown Source)
...
public class Sample {
public static void main(String[] args){
int c = 4 / 0;
}
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Test.main(Test.java:14)
4를 0으로 나눌 수가 없으므로 이와 같이 산술에 문제가 생겼다는 ArithmeticException 예외가 발생한다.
public class Sample {
public static void main(String[] args) {
int[] a = {1, 2, 3};
System.out.println(a[3]);
}
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at Test.main(Test.java:17)
a[3]은 a 배열의 4번째 값이므로 a 배열에서 구할 수 없다. 그래서 배열에서 아무것도 없는 곳을 가리켰다는 ArrayIndexOutOfBoundsException 예외가 발생했다.
예외 처리하기
try {
<수행할 문장 1>;
<수행할 문장 2>;
...
} catch(예외1) {
<수행할 문장 A>;
...
} catch(예외2) {
<수행할 문장 a>;
...
}
try 문 안의 수행할 문장 중에서 예외가 발생하지 않는다면 catch 문에 속한 문장들은 수행되지 않는다. 하지만 try 문 안의 문장을 수행하는 도중에 예외가 발생하면 예외에 해당되는 catch 문이 수행된다.
public class Sample {
public static void main(String[] args) {
int c;
try {
c = 4 / 0;
} catch(ArithmeticException e) {
c = -1; // 예외가 발생하여 이 문장이 수행된다.
}
}
}
ArithmeticException이 발생하면 c에 -1을 대입하도록 예외를 처리한 것이다. ArithmeticException e에서 e는 ArithmeticException 클래스의 객체, 즉 예외 객체에 해당한다. 이 예외 객체를 통해 해당 예외 클래스의 변수나 메서드를 호출할 수도 있다.
finally
public class Sample {
public void shouldBeRun() {
System.out.println("ok thanks.");
}
public static void main(String[] args) {
Sample sample = new Sample();
int c;
try {
c = 4 / 0;
sample.shouldBeRun(); // 이 코드는 실행되지 않는다.
} catch (ArithmeticException e) {
c = -1;
}
}
}
여기서 sample.shouldBeRun()는 절대로 실행될 수 없다. 왜냐하면 4/0에 의해 ArithmeticException이 발생하여 catch 구문으로 넘어가기 때문이다.
만약 sample.shouldBeRun() 메서드가 반드시 실행되어야 한다면 이런 경우를 처리하기 위해 자바에서는 다음과 같이 finally 문을 사용한다.
public class Sample {
public void shouldBeRun() {
System.out.println("ok thanks");
}
public static void main(String[] args) {
Sample sample = new Sample();
int c;
try {
c = 4 / 0;
} catch (ArithmeticException e) {
c = -1;
} finally {
sample.shouldBeRun(); // 예외에 상관없이 무조건 수행된다.
}
}
}
ok, thanks
finally 문은 try 문장 수행 중 예외 발생 여부에 상관없이 무조건 실행된다. 따라서 코드를 실행하면 sample.shouldBeRun() 메서드가 수행되어 ‘ok, thanks’라는 문장이 출력된다.
예외 활용하기 - RuntimeException과 Exception
public class Sample {
public void sayNick(String nick) {
if("바보".equals(nick)) {
return;
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("바보");
sample.sayNick("야호");
}
}
sayNick 메서드는 ‘바보’라는 문자열이 입력되면 return으로 메서드를 종료해 별명이 출력되지 못하도록 한다.
RuntimeException
class FoolException extends RuntimeException {
}
class FoolException extends RuntimeException {
}
public class Sample {
public void sayNick(String nick) {
if("바보".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("바보");
sample.sayNick("야호");
}
}
단순히 return했던 부분을 throw new FoolException()이라는 문장으로 변경하였다. 이 프로그램을 실행하면 ‘바보’라는 입력값으로 sayNick 메서드 실행 시 다음과 같은 예외가 발생한다.
Exception in thread "main" FoolException
at Sample.sayNick(Sample.java:7)
at Sample.main(Sample.java:14)
FoolException이 상속받은 클래스는 RuntimeException이다. 예외는 크게 두 가지로 구분된다.
- RuntimeException: 실행 시 발생하는 예외
- Exception: 컴파일 시 발생하는 예외
Exception은 예측이 가능한 경우에 사용하고, RuntimeException은 발생할 수도 있고 발생하지 않을 수도 있는 경우에 사용한다. 그래서 Exception을 Checked Exception, RuntimeException을 Unchecked Exception이라고도 한다.
Exception
RuntimeException을 상속하던 것을 Exception을 상속하도록 변경하자. 그런데 이렇게만 하면 Sample 클래스에서 컴파일 오류가 발생할 것이다. FoolException이 예측 가능한 Checked Exception으로 변경되어 예외 처리를 컴파일러가 강제하기 때문이다. 따라서 다음과 같이 변경해야 정상적으로 컴파일이 될 것이다.
class FoolException extends Exception {
}
public class Sample {
public void sayNick(String nick) {
try {
if("바보".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}catch(FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("바보");
sample.sayNick("야호");
}
}
이와 같이 컴파일 오류를 막기 위해서는 sayNick 메서드에서 try ~ catch 문으로 FoolException을 처리해야 한다.
예외 던지기
public class Sample {
public void sayNick(String nick) throws FoolException {
try { // try .. catch 문을 삭제할수 있다.
if("바보".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}catch(FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("바보");
sample.sayNick("야호");
}
}
sayNick 메서드 뒷부분에 throws라는 구문을 이용하여 FoolException을 위로 보낼 수가 있다(이를 ‘예외를 뒤로 미루기’라고도 한다.).
점프 투 자바throw와 throws의 차이
throw와 throws는 예외 처리와 관련된 키워드로 다음과 같은 차이점이 있다.
- throw: 메서드 내에서 예외를 발생시키는 데 사용된다.(예: throw new FoolException())
- thorws: 메서드 선언부에서 사용되며, 해당 메서드가 처리하지 않은 예외를 호출자에게 전달함을 나타낸다.(예: public void sayNick(String nick) throws FoolException)
이와 같이 sayNick 메서드를 변경하면 main 메서드에서 컴파일 오류가 발생할 것이다. throws 구문 때문에 FoolException의 예외를 처리해야 하는 대상이 sayNick 메서드에서 main 메서드(sayNick 메서드를 호출하는 메서드)로 변경되었기 때문이다. 따라서 컴파일 오류를 해결하려면 이번에는 다음과 같이 main 메서드를 변경해야 한다.
class FoolException extends Exception {
}
public class Sample {
public void sayNick(String nick) throws FoolException {
if("바보".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
try {
sample.sayNick("바보");
sample.sayNick("야호");
} catch (FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
}
}
FoolException이 발생했습니다.
main 메서드에서 try ~ catch 문으로 sayNick 메서드에 대한 FoolException 예외를 처리하였다.
이제 한 가지 고민이 남아 있다. FoolException 처리를 sayNick 메서드에서 하는 것이 좋을까? 아니면 throws를 이용하여 예외 처리를 main 메서드에서 하는 것이 좋을까? sayNick 메서드에서 처리하는 것과 main 메서드에서 처리하는 것에는 아주 큰 차이가 있다. sayNick 메서드에서 예외를 처리하는 경우에는 다음의 두 문장이 모두 수행된다.
sample.sayNick("바보");
sample.sayNick("야호");
물론 sample.sayNick("바보"); 문장 수행 시에는 FoolException이 발생하겠지만 그다음 문장인 sample.sayNick("야호"); 역시 수행된다.
하지만 main 메서드에서 예외 처리를 한 경우에는 두번 째 문장인 sample.sayNick("야호");가 수행되지 않는다. 왜냐하면 이미 첫번 째 문장에서 예외가 발생하여 catch 문으로 빠져버리기 때문이다.
try {
sample.sayNick("바보");
sample.sayNick("야호"); // 이 문장은 수행되지 않는다.
}catch(FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
이러한 이유로 프로그래밍할 때 예외를 처리하는 위치는 대단히 중요하다. 프로그램의 수행여부를 결정하기도 하고 다음에 배울 트랜잭션 처리와도 밀접한 관계가 있기 때문이다.
트랜잭션
갑자기 트랜잭션이라는 용어가 나와서 뜬금없다고 생각할 수도 있겠지만 트랜잭션(transaction)과 예외 처리는 매우 밀접한 관련이 있다. 트랜잭션과 예외 처리가 서로 어떤 관련이 있는지 알아보자.
트랜잭션이란 하나의 작업 단위를 뜻한다.
예를 들어 쇼핑몰의 ‘상품발송’이라는 트랜잭션을 가정해 보자. ‘상품발송’ 트랜잭션에는 다음과 같은 작업들이 있다.
- 포장
- 영수증 발행
- 발송
쇼핑몰의 운영자는 이 3가지 일 중 하나라도 실패하면 3가지 모두 취소하고 ‘상품발송’ 전의 상태로 되돌리고 싶어 한다.
스레드
동작하고 있는 프로그램을 프로세스(process)라고 한다. 보통 한 개의 프로세스는 한 가지의 일을 하지만, 스레드(thread)를 이용하면 한 프로세스 내에서 두 가지 또는 그 이상의 일을 동시에 할 수 있다.
Thread
public class Sample extends Thread {
public void run() { // Thread 를 상속하면 run 메서드를 구현해야 한다.
System.out.println("thread run.");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.start(); // start()로 쓰레드를 실행한다.
}
}
thread run.
Sample 클래스가 Thread 클래스를 상속했다. Thread 클래스의 run 메서드를 구현하면 sample.start() 메서드를 실행할 때 sample 객체의 run 메서드가 수행된다.
public class Sample extends Thread {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq + " thread start."); // 쓰레드 시작
try {
Thread.sleep(1000); // 1초 대기한다.
} catch (Exception e) {
}
System.out.println(this.seq + " thread end."); // 쓰레드 종료
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { // 총 10개의 쓰레드를 생성하여 실행한다.
Thread t = new Sample(i);
t.start();
}
System.out.println("main end."); // main 메서드 종료
}
}
총 10개의 스레드를 실행시키는 예제이다. 어떤 스레드인지 확인하기 위해서 스레드마다 생성자에 순서를 부여했다. 그리고 시작과 종료를 출력하게 했고 시작과 종료 사이에 1초의 간격이 생기도록 (Thread.sleep(1000)) 작성했다. 그리고 main 메서드 종료 시 ‘main end.’를 출력하도록 했다.
0 thread start.
4 thread start.
6 thread start.
2 thread start.
main end.
3 thread start.
7 thread start.
8 thread start.
1 thread start.
9 thread start.
5 thread start.
0 thread end.
4 thread end.
2 thread end.
6 thread end.
7 thread end.
3 thread end.
8 thread end.
9 thread end.
1 thread end.
5 thread end.
0번 스레드부터 9번 스레드까지 순서대로 실행되지 않고, 그 순서가 일정치 않은 것을 보면 스레드는 순서에 상관없이 동시에 실행된다는 사실을 알 수 있다. 더욱 재밌는 사실은 스레드가 종료되기 전에 main 메서드가 종료되었다는 사실이다. main 메서드가 종료될 때 ‘main end.’라는 문자열이 출력되는데 여기서는 중간쯤에 출력되어 있다.
Join
import java.util.ArrayList;
public class Sample extends Thread {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq+" thread start.");
try {
Thread.sleep(1000);
}catch(Exception e) {
}
System.out.println(this.seq+" thread end.");
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for(int i=0; i<10; i++) {
Thread t = new Sample(i);
t.start();
threads.add(t);
}
for(int i=0; i<threads.size(); i++) {
Thread t = threads.get(i);
try {
t.join(); // t 쓰레드가 종료할 때까지 기다린다.
}catch(Exception e) {
}
}
System.out.println("main end.");
}
}
생성된 스레드를 담기 위해서 ArrayList 객체인 threads를 만든 후 스레드 생성 시 생성된 객체를 threads에 저장했다. 그리고 main 메서드가 종료되기 전에 threads 객체에 담긴 각각의 스레드에 join 메서드를 호출하여 스레드가 종료될 때까지 대기하도록 했다. join 메서드는 스레드가 종료될 때까지 기다리게 하는 메서드이다. 코드를 변경한 후 프로그램을 실행하면 다음과 비슷한 결과가 나올 것이다.
0 thread start.
5 thread start.
2 thread start.
6 thread start.
9 thread start.
1 thread start.
7 thread start.
3 thread start.
8 thread start.
4 thread start.
0 thread end.
5 thread end.
2 thread end.
9 thread end.
6 thread end.
1 thread end.
7 thread end.
4 thread end.
8 thread end.
3 thread end.
main end.
여기서는 ‘main end.’ 문자열이 가장 마지막에 출력되는 것을 확인할 수 있다. 스레드를 활용한 프로그래밍을 할 때 가장 많이 실수하는 부분이 스레드가 종료되지 않았는데 스레드가 종료된 줄 알고 그다음 작업을 진행하게 만드는 일이다.
Runnable
Thread 객체를 만들 때 앞서 살펴본 예처럼 Thread 클래스를 상속하여 만들기도 하지만 주로 Runnable 인터페이스를 사용한다. 왜냐하면 Thread 클래스를 상속하면 Thread 클래스를 상속한 클래스가 다른 클래스를 상속할 수 없기 때문이다.
import java.util.ArrayList;
public class Sample implements Runnable {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq+" thread start.");
try {
Thread.sleep(1000);
}catch(Exception e) {
}
System.out.println(this.seq+" thread end.");
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for(int i=0; i<10; i++) {
Thread t = new Thread(new Sample(i));
t.start();
threads.add(t);
}
for(int i=0; i<threads.size(); i++) {
Thread t = threads.get(i);
try {
t.join();
}catch(Exception e) {
}
}
System.out.println("main end.");
}
}
Thread 클래스를 extends하던 것에서 Runnable 인터페이스를 implements하도록 변경했다.
그리고 Thread 객체를 생성하는 부분을 다음과 같이 변경했다.
Thread t = new Thread(new Sample(i));
Thread 객체의 생성자로 Runnable 인터페이스를 구현한 객체를 전달하는 방법을 사용한 것이다. 이렇게 변경된 코드는 이전에 만들었던 예제와 완전히 동일하게 동작한다. Thread 객체가 Thread 클래스를 상속했을 경우에는 다른 클래스를 상속할 수 없지만, 인터페이스를 사용한 경우에는 다른 클래스 상속이 가능하므로 좀 더 유연한 프로그램으로 만들 수 있다.
함수형 프로그래밍
자바는 Java 8 버전부터 함수형 프로그래밍을 지원하기 위해 람다(lambda)와 스트림(stream)이 도입되었다. 람다와 스트림을 사용하면 함수형 프로그래밍 스타일로 자바 코드를 작성할 수 있다. 물론 람다와 스트림을 사용하여 작성한 코드를 일반 스타일의 자바 코드로 바꾸어 작성하는 것이 불가능하지는 않다. 달리 말하면 람다와 스트림 없이도 자바 코드를 작성하는 데 어려움이 없다는 뜻이다. 그런데도 람다와 스트림을 사용하는 이유는 작성하는 코드의 양이 줄어들고 읽기 쉬운 코드를 만들 수 있기 때문이다.
람다
람다(lambda)는 익명 함수(anonymous function)를 의미한다. 일반적인 코드와 람다를 적용한 코드를 비교하며 람다에 대해서 자세히 알아보자.
일반적인 코드
다음과 같은 인터페이스를 보자. 두 개의 정수를 입력으로 받아 정수의 결괏값을 리턴하는 sum 함수를 정의한 인터페이스이다.
interface Calculator {
int sum(int a, int b);
}
class MyCalculator implements Calculator {
public int sum(int a, int b) {
return a+b;
}
}
Calculator 인터페이스를 구현한 MyCalculator 클래스를 생성하였다.
Calculator 인터페이스와 MyCalculator 클래스를 사용하여 자바 프로그램을 완성해 보자.
interface Calculator {
int sum(int a, int b);
}
class MyCalculator implements Calculator {
public int sum(int a, int b) {
return a+b;
}
}
public class Sample {
public static void main(String[] args) {
MyCalculator mc = new MyCalculator();
int result = mc.sum(3, 4);
System.out.println(result); // 7 출력
}
}
7
람다를 적용한 코드
interface Calculator {
int sum(int a, int b);
}
public class Sample {
public static void main(String[] args) {
Calculator mc = (int a, int b) -> a +b; // 람다를 적용한 코드
int result = mc.sum(3, 4);
System.out.println(result);
}
}
7
인터페이스 사용 시 주의 사항
여기서 주의해야 할 점은 Calculator 인터페이스의 메서드가 1개 이상이면 람다 함수를 사용할 수 없다는 점이다.
interface Calculator {
int sum(int a, int b);
int mul(int a, int b); // mul 메서드를 추가하면 컴파일 에러가 발생한다.
}
그래서 람다 함수로 사용할 인터페이스는 다음처럼 @FunctionalInterface 어노테이션을 사용하는 것이 좋다. @FunctionalInterface 어노테이션을 사용하면 다음처럼 2개 이상의 메서드를 가진 인터페이스를 작성하는 것이 불가능해진다.
@FunctionalInterface
interface Calculator {
int sum(int a, int b);
int mul(int a, int b); // @FunctionalInterface 는 두 번째 메서드를 허용하지 않는다.
}
람다 축약하기
앞서 작성한 람다를 적용한 자바 코드는 다음처럼 조금 더 축약이 가능하다. 인터페이스에 이미 입출력에 대한 타입이 정의되어 있으므로 입력값의 자료형인 int를 생략할 수 있다.
interface Calculator {
int sum(int a, int b);
}
public class Sample {
public static void main(String[] args) {
Calculator mc = (a, b) -> a +b;
int result = mc.sum(3, 4);
System.out.println(result);
}
}
스트림
스트림(stream)은 글자 그대로 해석하면 ‘흐름’이라는 뜻이다. 데이터가 물결처럼 흘러가면서 필터링 과정을 통해 여러 번 변경되어 반환되기 때문에 이러한 이름을 갖게 되었다.
int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};
이 배열에서 짝수만 뽑아 중복을 제거한 후에 역순으로 정렬하는 프로그램을 만들어 보자. 이 때 프로그램의 수행 결과는 다음과 같아야 한다.
int[] result = {8, 6, 4, 2};
import java.util.*;
public class Sample {
public static void main(String[] args) {
int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};
// 짝수만 포함하는 ArrayList 생성
ArrayList<Integer> dataList = new ArrayList<>();
for(int i=0; i<data.length; i++) {
if(data[i] % 2 == 0) {
dataList.add(data[i]);
}
}
// Set을 사용하여 중복을 제거
HashSet<Integer> dataSet = new HashSet<>(dataList);
// Set을 다시 List로 변경
ArrayList<Integer> distinctList = new ArrayList<>(dataSet);
// 역순으로 정렬
distinctList.sort(Comparator.reverseOrder());
// Integer 리스트를 정수 배열로 변환
int[] result = new int[distinctList.size()];
for(int i=0; i< distinctList.size(); i++) {
result[i] = distinctList.get(i);
}
}
}
정수 배열에서 짝수만을 찾아 ArrayList에 넣고 Set을 사용하여 중복을 제거한 후에 다시 Set을 리스트로 변환했다. 그리고 리스트의 sort를 사용하여 역순으로 정렬하고 정렬된 값으로 다시 정수 배열을 생성했다. 복잡하지만 보통 이와 비슷한 과정을 통해 문제를 해결해야 한다.
객관식 문제
1. java.util 패키지에 포함된 클래스가 아닌 것은? (정답:D)
- A) ArrayList
- B) HashMap
- C) Scanner
- D) File
2. 패키지 선언 키워드는 무엇인가요? (정답:A)
- A) package
- B) import
- C) include
- D) namespace
3. 다음 중 사용자 정의 패키지를 사용하는 올바른 방법은? (정답:C)
- A) include 패키지명;
- B) package 패키지명;
- C) import 패키지명.*;
- D) namespace 패키지명.*;
4. java.lang 패키지에 포함된 클래스는? (정답:D)
- A) String
- B) Math
- C) System
- D) 모두 해당
5. 다음 중 패키지와 관련 없는 것은? (정답:C)
- A) 클래스 간의 충돌 방지
- B) 클래스 조직화
- C) 접근 제어
- D) 네임스페이스 제공
6. import 문 없이 사용할 수 있는 클래스는? (정답:A)
- A) java.lang.String
- B) java.util.Scanner
- C) java.io.File
- D) java.net.Socket
7. 다음 코드를 실행했을 때 출력 결과는 무엇인가요? (정답:A)
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
- A) Hello, Java!
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않는다.
8. 다음 코드의 실행 결과는 무엇인가요? (정답:B)
package com.test;
public class Main {
public static void main(String[] args) {
int a = 10;
System.out.println(a + 5);
}
}
- A) 10
- B) 15
- C) 컴파일 오류 발생
- D) 런타임 오류 발생
9. 다음 코드 실행 결과는 무엇인가요? (정답:A)
package com.demo;
public class Test {
public static void main(String[] args) {
String packageName = "com.demo";
System.out.println("Package: " + packageName);
}
}
- A) Package: com.demo
- B) PackageName: com.demo
- C) 컴파일 오류 발생
- D) 런타임 오류 발생
10. 다음 코드의 실행 결과는 무엇인가요? (정답:A)
package com.example.util;
public class Test {
public static void main(String[] args) {
System.out.println("Util Test");
}
}
- A) Util Tes
- B) util test
- C) 컴파일 오류 발생
- D) 아무것도 출력되지 않음
11. 아래 코드의 실행 결과는 무엇인가요? (정답:A)
package invalid.package;
public class Test {
public static void main(String[] args) {
System.out.println("Invalid package!");
}
}
- A) Invalid package!
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
12. 다음 중 private 접근 제어자의 특징은? (정답:A)
A) 동일 클래스 내에서만 접근 가능
- B) 동일 패키지 내에서만 접근 가능
- C) 상속받은 클래스에서도 접근 가능
- D) 모든 클래스에서 접근 가능
정답: A) 동일 클래스 내에서만 접근 가능
13. protected 접근 제어자는 어디서 접근 가능한가요? (정답:C)
- A) 동일 클래스와 동일 패키지
- B) 동일 클래스와 하위 클래스
- C) 동일 클래스, 동일 패키지, 하위 클래스
- D) 모든 클래스
14. 접근 제어자가 없는 경우 기본 접근 수준은 무엇인가요? (정답:C)
- A) private
- B) protected
- C) default (패키지 접근 수준)
- D) public
15. 다음 중 public 접근 제어자의 설명으로 올바른 것은? (정답:C)
- A) 동일 클래스에서만 접근 가능
- B) 동일 패키지와 하위 클래스에서만 접근 가능
- C) 모든 클래스에서 접근 가능
- D) 동일 클래스와 동일 패키지에서만 접근 가능
16. 아래 코드에서 오류가 발생하는 이유는 무엇인가요? (정답:A)
package a;
public class A {
private int value = 10;
}
package b;
import a.A;
public class B {
public void printValue() {
A a = new A();
System.out.println(a.value);
}
}
- A) private 멤버는 동일 클래스에서만 접근 가능하기 때문
- B) 클래스 A가 public이 아니기 때문
- C) package 접근 수준의 제한 때문
- D) 컴파일러가 값을 초기화하지 않았기 때문
17. 다음 코드에서 컴파일 오류가 발생하는 경우는? (정답:A)
package package1;
public class ClassA {
int defaultVar = 5;
}
package package2;
import package1.ClassA;
public class ClassB {
public void printValue() {
ClassA a = new ClassA();
System.out.println(a.defaultVar);
}
}
- A) 패키지가 다르기 때문에 default 변수에 접근할 수 없다
- B) 클래스 A가 public이 아니기 때문
- C) default 접근 제어자는 하위 클래스에서만 접근 가능
- D) 컴파일 오류가 발생하지 않는다.
18. public 클래스와 메서드는 어디에서 접근 가능한가요?(정답:C)
- A) 동일 패키지에서만
- B) 상속받은 클래스에서만
- C) 모든 클래스에서
- D) 동일 클래스 내에서만
19. 접근 제어자 중 가장 제한적인 것은 무엇인가요?(정답:B)
- A) public
- B) private
- C) protected
- D) default
20. 다음 코드의 실행 결과를 고르시오.(정답:B)
package test;
public class Main {
private int value = 10;
public static void main(String[] args) {
Main m = new Main();
System.out.println(m.value);
}
}
- A) 10
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
21. 리스트 (정답:B)
package a;
public class A {
private void display() {
System.out.println("Private Method");
}
}
package b;
import a.A;
public class B {
public static void main(String[] args) {
A a = new A();
a.display();
}
}
- A) Private Method
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
22. 리스트 (정답:A)
package mypackage;
public class MyClass {
protected void show() {
System.out.println("Protected Method");
}
}
package another;
import mypackage.MyClass;
public class Test extends MyClass {
public static void main(String[] args) {
Test t = new Test();
t.show();
}
}
- A) Protected Method
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
23. 리스트 (정답:B)
package p1;
public class A {
int defaultVar = 100;
}
package p2;
import p1.A;
public class B {
public static void main(String[] args) {
A a = new A();
System.out.println(a.defaultVar);
}
}
- A) 100
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
24. 리스트 (정답:A)
package test;
public class Main {
public static void main(String[] args) {
Helper.display();
}
}
class Helper {
static void display() {
System.out.println("Helper Method");
}
}
- A) Helper Method
- B) 컴파일 오류 발생
- C) 런타임 오류 발생
- D) 아무것도 출력되지 않음
25. static 키워드의 주요 특징은 무엇인가요? (정답:B)
- A) 클래스와 인스턴스에서 모두 접근 가능하다
- B) 클래스 로드 시점에 메모리에 할당된다
- C) 객체 생성 없이 사용 불가능하다
- D) 쓰레드마다 별도로 메모리에 할당된다
26. static 키워드가 사용될 수 없는 곳은? (정답:B)
- A) 클래스 변수
- B) 인스턴스 변수
- C) 메서드
- D) 클래스 내부 블록
27. 다음 중 static 키워드로 선언된 메서드의 특징은? (정답:B)
- A) 인스턴스 변수를 참조할 수 있다
- B) 클래스 이름을 통해 호출할 수 있다
- C) this 키워드를 사용할 수 있다
- D) 상속할 수 없다
28. 다음 코드의 출력 결과는? (정답:D)
public class Test {
static int count = 0;
public Test() {
count++;
}
public static void main(String[] args) {
new Test();
new Test()
System.out.println(count);
}
}
- A) 0
- B) 1
- C) 2
- D) 컴파일 오류
29. static 메서드에서 사용할 수 없는 것은? (정답:A)
- A) this 키워드
- B) static 변수
- C) 클래스 이름
- D) static 메서드
30. static 블록이 여러 개인 경우 실행 순서는? (정답:A)
- A) 선언된 순서대로 실행된다
- B) 역순으로 실행된다
- C) JVM이 임의로 결정한다
- D) 첫 번째 블록만 실행된다
31. 다음 코드에서 컴파일 오류가 발생하는 이유는? (정답:A)
public class Test {
static void print() {
System.out.println(value);
}
int value = 10;
}
- A) static 메서드에서 인스턴스 변수를 사용할 수 없다
- B) value가 초기화되지 않았다
- C) static 메서드는 호출할 수 없다
- D) 변수 이름 충돌이 발생했다
32. 다음 중 static 키워드의 올바른 사용 예는? (정답:C)
- A) 매번 새로운 객체를 생성해야 할 때
- B) 특정 인스턴스에 데이터를 유지해야 할 때
- C) 공통 데이터를 여러 객체가 공유해야 할 때
- D) 메모리 할당을 동적으로 조정해야 할 때
33. 다음 코드의 실행 결과를 고르시오. (정답:B)
public class Test {
static int count = 0;
static {
count = 10;
}
public static void main(String[] args) {
System.out.println(count);
}
}
- A) 0
- B) 10
- C) 컴파일 오류 발생
- D) 런타임 오류 발생
34. 다음 코드의 실행 결과를 고르시오. (정답:A)
public class Test {
static void display() {
Sysem.out.println("Static Method");
}
public static void main(String[] args) {
Test t = null;
t.display();
}
}
- A) Static Method
- B) NullPointerException 발생
- C) 컴파일 오류 발생
- D) 아무것도 출력되지 않음
35. 다음 코드의 실행 결과를 고르시오.(정답:C)
public class Test {
static int count;
static {
count = 20;
}
public static void main(String[] args) {
count += 5;
System.out.println(count);
}
}
- A) 0
- B) 5
- C) 25
36. 다음 코드의 실행 결과를 고르시오. (정답:B)
public class Test {
static int x = 10;
public static void main(String[] args) {
int x = 20;
System.out.println(x);
System.out.println(Test.x);
}
}
- A) 10, 10
- B) 20, 10
- C) 20, 20
- D) 컴파일 오류 발생
코테
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String S = sc.nextLine();
int[] firstPosition = new int[26];
java.util.Arrays.fill(firstPosition, -1);
for (int i = 0; i < S.length(); i++) {
int index = S.charAt(i) - 'a';
if (firstPosition[index] == -1) {
firstPosition[index] = i;
}
}
for (int pos : firstPosition) {
System.out.print(pos +" ");
}
sc.close();
}
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
boolean[] remainders = new boolean[42];
int count = 0;
for (int i = 0; i < 10; i++) {
int num = sc.nextInt();
int remainder = num % 42;
if (!remainders[remainder]) {
remainders[remainder] = true;
count++;
}
}
System.out.println(count);
sc.close();
}
}
'WINK-(Web & App) > JAVA 스터디' 카테고리의 다른 글
[2024-2 Java 스터디] 김재승 #6주차 (0) | 2024.11.22 |
---|---|
[2024-2 Java 스터디] 김태일 #6주차 (0) | 2024.11.21 |
[2024-2 Java 스터디] 이가인 #6주차 (1) | 2024.11.21 |
[2024-2 Java 스터디] 김민서 #6주차 (1) | 2024.11.20 |
[2024-2 Java 스터디] 강보경 #6주차 (0) | 2024.11.20 |