レポート課題

Java 6 を使って作りなさい。

レポートには表紙をつけてください。

変更履歴

2011/5/18
課題1初出
2011/5/20
課題1訂正
2011/5/26
課題1-3にヒントを追加
2011/6/4
Kadai3Test の不備を訂正
2011/6/2
Dollar2 のコンストラクタを public に訂正、 課題1-3にさらなるヒントを追加
2011/6/7
Kadai3Test のミスを訂正
2011/6/7
課題2-2 の size を private から protected に訂正
2011/12/7
課題2-2のsizeをfinalにし、abstractの方を size2に名前変更。 これに関しては、旧問題で解答、提出しても良い。

課題1

4年生
2011年6月8日20:00
2,3年生
2011年11月30日20:00

提出先: レポートボックス

通貨を使うクラスを考えます。 値を入れると、通貨記号を含めて表示し、また円レートを設定すると、等価な円オブジェクトを返します。

以下の設問において kaitou パッケージを作り、その中に指定されたクラスを 作成しなさい。

なお、テストクラスを用意したので、各設問 において、指示のあるテストクラスを用いてテストを行い、正常であった旨を 報告しなさい。

問1-1

始めに、コンストラクタに値を入れ、toString で通貨記号付きの文字列を得 られるようにします。 interface は空ですが、 toString は java.lang.Object に登録されているの で使用できます。

kadai/Money1.java


package kadai;
public interface Money1 {
}

この時、この Money1 を implements し、次の Yen1, Dollar1 クラスの親クラ スになる kaitou.AbstractMoney1 クラスを定義しなさい。

kadai/Yen1.java


package kadai;
import kaitou.AbstractMoney1;
public class Yen1 extends AbstractMoney1 {
	public Yen1(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "";
	}
	@Override
	protected String getPostfix() {
		return "円";
	}
}

kadai/Dollar1.java


package kadai;
import kaitou.AbstractMoney1;
public class Dollar1 extends AbstractMoney1 {
	public Dollar1(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "$";
	}
	@Override
	protected String getPostfix() {
		return "";
	}
}

これらに対して、次のようにすると「100.0円」とか「$3.0」が出力されるようにするということです。


import kadai.Dollar1;
import kadai.Money1;
import kadai.Yen1;
public class Test1 {
	public static void main(String[] args) {
		Money1 m1 = new Yen1(100);
		Money1 m2 = new Dollar1(3);
		System.out.println(m1);
		System.out.println(m2);
	}
}

なお、これらをテストするために Yen1Test と Dollar1Test を使用しなさい。

ヒント

テンプレートメソッドを使用します。

問1-2

次に、円レートを設定して、円に換算する仕組みを作ります。 Money2 インターフェイスは Money1 を継承し、getRate メソッドと、換算し た Yen2 が得られる getYen メソッドが定義されます。

kadai/Money2.java


package kadai;
public interface Money2 extends Money1 {
	double getRate();
	Money2 getYen();
}

前問同様のクラスを次のように定義します。 このクラスが正常に動作するように kaitou.AbstractMoney2 を作りなさい。 但し、kaitou.AbstractMoney2 は kaitou.AbstractMoney1 を継承し、 kadai.Money2 を implements しなさい。

kadai/Yen2.java


package kadai;
import kaitou.AbstractMoney2;
public class Yen2 extends AbstractMoney2 {
	public Yen2(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "";
	}
	@Override
	protected String getPostfix() {
		return "円";
	}
	@Override
	public double getRate() {
		return 1.0;
	}
	public static void setYenRate(double r) {
	}
}

kadai/Dollar2.java


package kadai;
import kaitou.AbstractMoney2;
public class Dollar2 extends AbstractMoney2 {
	public Dollar2(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "$";
	}
	@Override
	protected String getPostfix() {
		return "";
	}
	private static double rate;
	@Override
	public double getRate() {
		return rate;
	}
	public static void setYenRate(double r) {
		rate=r;
	}
}

つまり、これにより次のように 3 ドルを240円に換算できます。


import kadai.Dollar2;
import kadai.Money2;
public class Test2 {
	public static void main(String[] args) {
		Money2 m = new Dollar2(3);
		System.out.println(m);
		Dollar2.setYenRate(80);
		System.out.println(m.getYen());
	}
}

なお、これらをテストするクラス Yen2Test と Dollar2Test を使用しなさい。

問1-3

次に、大小関係を比較できるようにします。 まず、kadai.Money3 インターフェイスは Complarable を継承します。 そのため、値を比較できるようにするため、 getValue メソッドを追加します。 また、 getYen の戻す値の型を Money3 に上書きします。

kadai/Money3.java


package kadai;
public interface Money3 extends Money2 ,Comparable<Money3>{ 
	double getValue();
	Money3 getYen();
}

つぎに、前問同様のクラスを次のように定義します。 但し、新たに kadai.Euro クラスを導入しています。 このクラスが正常に動作するように kaitou.AbstractMoney3 を作りなさい。 但し、kaitou.AbstractMoney3 は kaitou.AbstractMoney2 を継承し、 kadai.Money3 を implements しなさい。 また、 java.lang.Comparable のマニュアルにあるように、 equals メソッド、 hashCode メソッドも実装しなさい。 なおこれらに関しては Eclipse の自動生成の機能を利用しても構わない。

ヒント(2011/05/26)

equals のシグネチャは boolean equals(Object obj) なので、引数は Object 型です(java.lang.Object のマニュアル参照)。 お金が等しいことを金額が等しいこととするためには、両者を金額比較する必 要があります。 そのためには、 obj を Money3 にキャストする必要があります。 キャストできるかどうかを判定するには、 obj instanceof Money3 でチェッ クします。

ヒント(2011/06/02)

AbstractMoney3 そのものは、タダでは equals を自動生成できません。 それはAbstractMoney3 はフィールドを持たないからです。 しかし、実際は親クラスの protected なフィールドを参照するような equals を作成したいわけです。 そのため、Eclipse を騙すと、equals を生成してくれます。 つまり、親クラスの protected なフィールドと同じ名前のフィールドを一旦 AbstractMoney3 クラスに書いてから equals を生成し、そのフィールドを消すと、親クラスのフィールドを参照する equals が生成されます。 また、生成するとき、 instanceof を使用するように指定することを忘れずに。

kadai/Yen3.java


package kadai;
import kaitou.AbstractMoney3;
public class Yen3 extends AbstractMoney3 {
	public Yen3(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "";
	}
	@Override
	protected String getPostfix() {
		return "円";
	}
	@Override
	public double getRate() {
		return 1.0;
	}
	public static void setYenRate(double r) {
	}
}

kadai/Dollar3.java


package kadai;
import kaitou.AbstractMoney3;
public class Dollar3 extends AbstractMoney3 {
	public Dollar3(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "$";
	}
	@Override
	protected String getPostfix() {
		return "";
	}
	private static double rate;
	@Override
	public double getRate() {
		return rate;
	}
	public static void setYenRate(double r) {
		rate=r;
	}
}

kadai/Euro.java


package kadai;
import kaitou.AbstractMoney3;
public class Euro  extends AbstractMoney3 {
	public Euro(double value) {
		super(value);
	}
	@Override
	protected String getPrefix() {
		return "";
	}
	@Override
	protected String getPostfix() {
		return "Euro";
	}
	private static double rate;
	@Override
	public double getRate() {
		return rate;
	}
	public static void setYenRate(double r) {
		rate=r;
	}
}

なお、これらをテストするクラス Kadai3Test を使用しなさい。

問1-4

次のプログラムを動作させ、プログラムの動きを説明しなさい。

kadai/Test.java


package kadai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kadai.Dollar3;
import kadai.Euro;
import kadai.Money3;
import kadai.Yen3;
public class Test {
	public static void main(String[] args) {
		List<Money3> moneyList = new ArrayList<Money3>();
		moneyList.add(new Yen3(100));
		moneyList.add(new Yen3(150));
		moneyList.add(new Yen3(200));
		moneyList.add(new Dollar3(1));
		moneyList.add(new Dollar3(2));
		moneyList.add(new Dollar3(3));
		moneyList.add(new Euro(1));
		moneyList.add(new Euro(2));
		moneyList.add(new Euro(3));
		System.out.println(moneyList);
		Dollar3.setYenRate(80);
		Euro.setYenRate(115);
		Collections.sort(moneyList);
		System.out.println(moneyList);
		List<Money3> moneyList2 = new ArrayList<Money3>();
		for(Money3 m : moneyList){
			moneyList2.add(m.getYen());
		}
		System.out.println(moneyList2);
	}
}

おまけ

Eclipse のセッティングの方法

  1. Java のプロジェクトを作成する
  2. 右クリックで properties を選択し、Text file encoding を UTF-8 にす る。
  3. 課題ファイル集(6月4日版)をダウンロードする。
  4. プロジェクトを右クリックし、import を選ぶ
  5. General の中の Archive file を選ぶ
  6. ダウンロードしたファイルを選択すると、 src の中にファイルが取り込 まれる
  7. kadai.Dollar1Test の画面の org.junit の波線付近にカーソルを当て、 fix Project setup を選ぶと、Add JUnit4 Library ... の表示が出るので、 Ok を押す

課題2

4年生
2011年6月29日20:00
2,3年生
2012年1月11日20:00

提出先: レポートボックス

順序木を利用し、キーの値で値を整列するデータ構造を作成することを考えま す。 そのためには、java.util.Map を実装し、 java.util.AbstractMap を継承し、 最小限のプログラムで動作させます。 まず、文字列をキーとして、整数値を整列するような TDUMap3 を目標としま す。 完成した後、Generics を利用した任意の型のキーと値を使用できる TDUMap を作成します。

こちらで用意したプログラムはすべて kadai フォルダに入っており、 kadai パッケージに含まれます。 また、テストプログラムを用意したので、活 用すること。 解答のプログラムは kaitou パッケージに作成しなさい。

発展問題の解答は任意です。

課題2-1

はじめに、型 E の配列を受け取って java.util.Set<E> として動作する kadai.TDUSet<E> を作成します。 これは java.util.AbstractSet<E> を継承し、配列を受け取るコンスト ラクタと、 size() メソッドと iterator メソッドを実装するだけで実現できます。 なお、この TDUSet に関しては add や remove などの要素を変更するような実装は 一切しないことにします。 そのため、下記のように実装しました。

kadai.TDUSet


package kadai;
import java.util.AbstractSet;
import java.util.Iterator;

import kaitou.TDUIterator;

public class TDUSet<E> extends AbstractSet<E>{
	private E[] set;
	public TDUSet(E[] array){
		set = array;
	}
	@Override
	public int size(){
		return set.length;
	}
	@Override
	public Iterator<E> iterator(){
		return new TDUIterator<E>(set);
	}
}

これが動作するように kaitou.TDUIterator<E> を作成しなさい。 なお、 public void remove() に関しては、何もしないメソッドて構いません。 これをテストするために、 TDUSetTest を利用しなさい。

課題2-2

次に、 java.util.Map<String,Integer> として動作するクラスを作成 することを考えます。 そのため、まず、java.util.Map.Entry<String,Integer> として動作し、 二分木の節となるクラス TDUMapEntry1 を、 java.util.AbstractMap.SimpleEntry<String,Integer>を継承し、 下記のように作成しました。

kadai.TDUMapEntry1


package kadai;
import java.util.AbstractMap;
public class TDUMapEntry1 extends AbstractMap.SimpleEntry<String,Integer>
{
	private static final long serialVersionUID = 1L;
	public TDUMapEntry1(String s ,Integer i){
		super(s,i);
		left=null;
		right=null;
	}
	public TDUMapEntry1 left;
	public TDUMapEntry1 right;
}

次に、TDUMap1 を java.util.AbstractMap<String,Integer> を継承し て作成することを考えます。 但し、このクラスは TDUMapEntry1 で作られた二分木に対して、サイズ(節点 の数)を返すメソッド int size() を実装することだけを目標として考えます。 まずkadai.AbstractTDUMap1 を下記のように作成しました。

AbstractTDUMap1


package kadai;
import java.util.AbstractMap;
import java.util.Set;
import java.util.Map;
public abstract class AbstractTDUMap1 extends AbstractMap<String,Integer> {
	protected TDUMapEntry1 root;
	protected AbstractTDUMap1(){
		root=null;
	}
	protected AbstractTDUMap1(TDUMapEntry1 root){
		this.root = root;
	}
	@Override
	final public int size(){
		return size2(root);
	}
	@Override 
	public Set<Map.Entry<String,Integer>> entrySet(){
		return null;
	}
	protected abstract int size2(TDUMapEntry1 p);
}

これを継承し、二分木のサイズがきちんと返るように privateprotected int size2(TDUMapEntry1 p) という関数を実装したクラス kaitou.TDUMap1 を作成しなさい。 これのテストには、 TDUMap1Test を使用しなさい。 なお、レポートに報告するには、この TDUMap1Test の size() メソッド呼び 出し箇所(5箇所)におけるデータの木構造を図示したもの作成し、それぞれ size がどのように動作するか説明すること。

課題2-3

次に、 TDUMap1 を継承し、 entrySet() を正常に出力するようなクラス TDUMap2 を作ります。 もともと、java.util.AbstractMap は、この entrySet() だけを実装すれば動 作するようになっていますので、これを実装することで正常な java.util.Map として動作するようになります。 entrySet() を作成するために次のアルゴリズムを考えます。 大まかな方針は、まずTDUMapEntry1 の木構造から java.util.Map.Entry<String,Integer> の配列を作り、次に TDUSet に渡 したものを 戻り値として返すことです。

  1. 木構造に対してサイズを求め、そのサイズ分の配列 Map.Entry<String,Integer> [] を作成する
  2. 配列用の index を 0 に初期化する
  3. 木を一番左の要素から順番にたどるようなプログラム travarse を呼び出 し、木の要素を順に配列に貯める
  4. TDUSet<Map.Entry<String,Integer>> に作成した配列を与え て entrySet の値として返す

上記に基づいて途中まで作成したのが下記の AbstractTDUMap2 クラスです。 これを継承し、正常に動作するようにTDUMap2を作成しなさい。

kadai.TDUMap2


package kadai;
import java.util.Map;
import java.util.Set;
import kaitou.TDUMap1;
public abstract class AbstractTDUMap2 extends TDUMap1 {
	protected AbstractTDUMap2(){
		super();
	}
	protected AbstractTDUMap2(TDUMapEntry1 root){
		super(root);
	}
	protected Map.Entry<String,Integer>[] array;
	protected int index;
	@SuppressWarnings("unchecked")
	@Override
	public Set<Map.Entry<String,Integer>> entrySet(){
		array = (Map.Entry<String,Integer>[]) new Map.Entry[size()];
		index=0;
		traverse(root);
		return new TDUSet<Map.Entry<String,Integer>>(array);
	}
	protected abstract void traverse(TDUMapEntry1 p);
}

これが正常に動作することを確かめるために、TDUMap2Testを使用しなさい。 なお、プログラムの説明には、TDUMap2Testの各 entrySet() を呼び出すとき の木構造と、 traverse で作成する配列をそれぞれ図示したもの作成し、 traverse がどのように動作するか説明すること。

課題2-4

最後に TDUMap3 として put メソッドにより要素を追加できるように実装し ます。 put は次のように実装します。

  1. TDUMapEntry1(s,i) を作り、変数 e により参照する。
  2. root が null なら、 root=e とし、 null を返す。
  3. put2(root,e) を呼び出し、戻り値を 返す。 ここで戻り値とは、値を更新する時は保存してあった古い値とし、新規の値を 保存する場合は null とする。 また、 put2(root,e) は実際に順序木を作りデータをしまうメソッドである。

ここまでを実装したクラス kadai.AbstractTDUMap3 を以下に示します。 これを継承し、 protected Integer put2(TDUMapEntry p, TDUMapEntry e) を実装したクラス TDUMap3 を作りなさい。 なお、この実装する put2(TDUMapeEntry p, TDUMapEntry e) の仕様は次の通りです。

  1. p の指している getKey の値と e の指している getKey の値が等価なら、 p の Value を e の getValue の値に更新して、もともとあった値を返す。
  2. p の指している getKey の値より e の指している getKey の値が小さい なら、 p.left に関して処理を行う。
    1. p.left が null なら p.left に e をつないで null を返す。
    2. さもなければ put2(p.left,e) の値を返す
  3. 大きいときは p.right に対して同様の処理を行う

kadai.AbstractTDUMap3


package kadai;
import kaitou.TDUMap2;
public abstract class AbstractTDUMap3 extends TDUMap2 {
	protected AbstractTDUMap3(){
		super();
	}
	@Override
	public Integer put(String s, Integer i){
		final TDUMapEntry1 e = new TDUMapEntry1(s,i);
		if(root==null){
			root=e;
			return null;
		}
		return put2(root,e);
	}
	protected abstract Integer put2(TDUMapEntry1 p, TDUMapEntry1 e);
}

作成したプログラムが正常に動作するかどうか、TDUMap3Testを使用して確かめなさい。 なお、TDUMap3Test において TDUMap3 の初期状態、 test("jkl",12) の呼び出し、 test("def",456) の呼び出し、 test("pqr",678) の呼び出し、 test("mno",567) の呼び出しに関して、TDUMap3 の内 部の木の構造を示し、 put2 がどのように動作するかを木の構造を用いて説明 しなさい。

発展問題2-5

Generics を使用し、任意の型 K, V を登録できる TDUMap<K extends Comparable<? super K>,V> を作ることを考える。 これを実現するため、Generics に対応した kadai.TDUMapEntry<K,V> を次のように定義する。


package kadai;
import java.util.AbstractMap;
public class TDUMapEntry<K,V> extends AbstractMap.SimpleEntry<K,V>
{
	private static final long serialVersionUID = 1L;
	public TDUMapEntry(K s ,V i){
		super(s,i);
		left=null;
		right=null;
	}
	public TDUMapEntry<K,V> left;
	public TDUMapEntry<K,V> right;
}

これを用いて、TDUMap を作成しなさい。 なお、作成すべきメソッド、フィールドなどは概ね次のようになるはずです。


package kaitou;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Set;
import kadai.TDUMapEntry;
import kadai.TDUSet;
public class TDUMap<K extends Comparable<? super K>, V> extends AbstractMap<K, V> {
	private TDUMapEntry<K,V> root;
	public TDUMap(){...}
	public TDUMap(TDUMapEntry<K,V> root){...}
	@Override
	public int size(){...}
	private int size(TDUMapEntry<K,V> p) {...}
	private Entry<K, V>[] array;
	private int index;
	@SuppressWarnings("unchecked")
	@Override
	public Set<Entry<K, V>> entrySet(){...}
	private void traverse(TDUMapEntry<K,V> p) {...}
	@Override
	public V put(K s, V i){...}
	private V put2(TDUMapEntry<K,V> p, TDUMapEntry<K,V> e) {...}
}

これが正常に動作することを TDUMapTestによりテスト結果を 報告しなさい。

発展問題2-6

下記のプログラム kadai.TDUMapDemo がどのようなプログラムか説明しなさい。 そして、HashMap, TreeMap, TDUMap についてそれぞれ 5 回測定し、結果を比較しなさい。 また、どうしてそのような結果になったかを考察しなさい。

kadai.TDUMapDemo.java


package kadai;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;
import kaitou.TDUMap;
class StopWatch {
	private Date now ;
	public StopWatch() {
		now=new Date() ;
	}
	@Override
	public String toString(){
		Date next=new Date();
		double time=(double)(next.getTime()-now.getTime())/1000.0 ;
		now=next;
		return String.valueOf(time);
	}
}
public class TDUMapDemo {
	final private static int n = 10000;
	final private static int wordLength = 5;
	public static void main(String[] args) {
		int k = Integer.valueOf(args[0]);
		@SuppressWarnings("unchecked")
		Map<String,Double>[] maps = (Map<String, Double>[])(new Map[]{
				new HashMap<String,Double>(),
				new TreeMap<String,Double>(),
				new TDUMap<String,Double>()
		});
		String[] testWords = generateStrings(n);
		double[] testDoubles = generateDouble(n);
		StopWatch sw = new StopWatch();
		test(maps[k],testWords,testDoubles);
		System.out.println(maps[k].getClass().getName()+":"+sw);
	}
	private static void test(Map<String, Double> m, String[] testWords,
			double[] testDoubles) {
		for(int i=0; i<testWords.length; i++){
			m.put(testWords[i], testDoubles[i]);
			m.entrySet();
		}
	}
	private static double[] generateDouble(int n2) {
		double[] result = new double[n2];
		for(int i=0 ; i<n2; i++){
			result[i]=Math.random();
		}
		return result;
	}
	private static String[] generateStrings(int n2) {
		String[] result = new String[n2];
		for(int i=0; i<n2; i++){
			result[i]=generateString(wordLength);
		}
		return result;
	}
	final private static String alphabets = "abcdefghijklmnopqrstuvwxyz";
	private static String generateString(int i) {
		StringBuilder result = new StringBuilder();
		for(int j=0; j<i; j++){
			result.append(alphabets.charAt((int)(Math.random()*alphabets.length())));
		}
		return result.toString();
	}
}

プログラム

上記のプログラムをまとめたものを ダウンロード できます。

課題2のレポート作成上の注意

プログラムの動作の説明において、特定のデータ構造を例示し、図示して説明 すること。 但し、図とプログラムに矛盾がある場合、不合格になります。

今回の合否の分水嶺は、課題 2-2, 2-3, 2-4 の再帰のプログラムの説明にお ける終了条件の説明がプログラムと一致しているか否かという点であろうと考えております。 ご注意下さい。

採点基準

  1. 問題をちゃんと理解すること。 特に解答が楽になるようにとか、「わかりやすく」とか「しっかり」など、非 科学的な理由で勝手に問題を作り替えてはいけません。
  2. プログラムが正常に動作すること。 指定した出力を正確に出すこと。 また、指定していない表示を行わないこと。 さらに、多少の動作条件の変化に対して、正常に対応できること。 特に、テストに対しては正常に動作しても、考えうる他の正常な入力に対して暴走するようなプログラムは不可です。
  3. 実行例を付けること。 実行可能なプログラムに関して、正しく実行された実行例を必ずつけること。 なお、問題の趣旨を理解しており、膨大な出力のうち、全ての出力が必ずしも 必要ないと判断される場合は、出力の一部を省略しても良い。
  4. 説明が適切であること。 プログラムの内容を正しく説明していなければなりません。 また、テストの内容を理解し、テスト結果からプログラムが正常であることを 理由を付けて判定すること。

レポート作成上の注意点

  1. 表紙をつけてください。 また、再提出時は前回の表紙をつけてください。
  2. プログラミングのレポートでは必ずプログラムの説明をすること。 その時に、一行一行を日本語に直訳するのではなく、データの読み込みとか、 出力とかの部分に分割し、機能毎に使用した手法を説明すること。 プログラム中にコメントを入れてもプログラムの説明とはみなさないので注意 する事。 プログラムの説明ではつぎのように説明をしてください。
    1. プログラム全体の構成
    2. 各部分の機能
    3. それぞれの機能を実現するために行ったプログラミング上の工夫
  3. 「問題を解きなさい」という問に対して「解きました。合ってました」で は正解ではないことはわかるはず。 「テストしなさい」という問に対しては、テストの方法の説明、実際のテスト の実施方法、テスト結果、検証などを説明して下さい。
  4. レポートは手書きでもワープロでも構いません。但し、実行結果はコン ピュータの出力を添付すること。 また、なるべく白黒で作成すること。実行結果などでどうしても色が付いてしまうような場合はそのままで構いません。 実行結果が無いレポートは不合格です。
  5. 考察は必ず書いて下さい。
  6. 不必要なことはなるべく書かない事。 長過ぎるレポートは減点します。またなるべく両面印刷にしてください。 但し、文字は必要以上に小さくしない事。レポート本文の文字は 10 ポイント 以上のものを使う事。

なお、写したと思われるほど酷似したレポートが複数提出された場合、原著が どれかの調査を行わず、抽選で一通のレポートのみを評価 の対象とし、他は提出済みの不合格レポートとして再提出は課しません。 自分で意図せずに他人にコピーされてしまった場合も同様ですので、レポート の取り扱いについては十分に注意して下さい。


坂本直志 <[email protected]>
東京電機大学工学部情報通信工学科