电工实习两周,算是抽空把Java的大作业搞好了。感悟还是蛮多的,遂记录之。
原来看《thinking in Java》的时候是完全不懂GUI,不单如此,很多之前看过的知识由于没有应用到具体项目中,时间一长竟然忘了许多。以《thinking in Java》如此丰富高深的内容,涉及各种design pattern和 类机制,其实真正会用的真不多。。。现在觉得可以重新回头看看要点了。
老实说吧,刚开始的时候真是各种不适应。用惯了C的人,代码越简洁越好,但是面对Java这一坨坨的类。。。好吧,确实有点难以接受咯。如果不是有eclipse这种自动化程度这么高的IDE,而是用什么notepad ++,估计早就崩溃了 = =
敲代码过程中主要还是吃老本,Java的特性没用到多少,反倒是有点C++的OOP影子。但是基础方面我觉得两者差不多吧,都是OOP的话。
说回五子棋。其实核心的也就是AI算法。基本思路是用二维数组模拟棋盘状况,然后对每个可能下子的空位置进行评价,也就是在最有价值的位置下子。这个价值判定分两部分,对己方的有利程度和对敌方的有利程度。对一个位置的考虑是有主攻和主防两种方向。实现起来也不算难,就是根据相同的权值表对该位置进行两种角色的评价。但是这个算法其实还远远不够,顶多只能算考虑了一步半(比只考虑进攻的要好点)。考虑过A*寻路算法+启发式搜索,还有博弈树,但是无奈水准还不够啊,真要弄起来估计deadline之前完成不了,遂暂且作罢。但是这个思路我觉得还是不错的,就是实现起来可能会比较麻烦。
另外还有一点深有体会的就是各组件之间触发事件的相互响应。Java的事件机制感觉上比较混乱,而且功能不够齐全,有些还有自己实现。各部件之间基本上是通过父部件来进行通信。e.g. A component 的listener接受到触发信号,然后需要通过parent component来调用B component的函数。有时候直接用getparent方法还不行,要在构造的时候直接传入父部件的引用,这样貌似效果要好点。
类的设计也是很讲究技巧的。怎样才能体现程序的高内聚,低耦合呢,这里貌似有点类似递归的思想。把大任务分成很多的小任务,由对应的对象完成。也就是说,作为某一类对象,其自身拥有的特殊属性和操作应该封装起来。这个就好像DIY,父部件把任务分配给子部件,至于怎么完成就是子部件的事了,父部件关心的只是任务完成的结果。如果是大量重复的子部件,更应该仔细考虑这个问题,尽量提高代码复用性,同时注意节约内存空间。这些都体现在代码的细节上。
前期的话,基本上是变开发边重构的。因为本身就不熟悉Java,同时会参考另外的样例程序。通过比较代码,了解原作者的思路,同时思想为什么要这样写,有什么好处,是否有更好的实现方法。经过不断的对比debug,算是搞出一个相对精简的AI算法。效果上来说还过得去吧。还有就是蛋疼的UI布局管理器,这里就不多说。各种触发事件之间的协调变化也是要慢慢理清思路才好下手,反正都是细节问题。
要说开发这个程序,主要目的还是熟悉基本的UI设计与操作,事件机制和OOP思想。现在看来,预期目标算是基本达到了,良好的核心算法是需要扎实的算法基本功和建模能力的,这个在不断加强中。
Anyway,熟悉软件开发流程算是暂告一段落。貌似各种书和资料(PPT,pdf)什么的已经积压的很多了,还有得忙呢。期待暑假的锻炼~
源码已托管于Google Code:
code.google.com/p/gobang-scut-csne/
为开源社区贡献的第一份代码 T_T
2012年5月21日星期一
2012年3月18日星期日
引用(别名)是什么?
由一个简单的JAVA问题引发了一场热情洋溢的群众喜闻乐见的讨论,同时扯到C++上。。。
先上代码:
public class test {
public static void main(String[] args){
int a = 4;
int b = a;
String s1 = "abc";
String s2 = s1;
String s3 = "def";
String s4 = "def";
System.out.println(a == b); // 1
System.out.println(s1 == s2); // 2
System.out.println(s3 == s4); // 3
s3 = "abc";
System.out.println(s1 == s3); // 4
s2 = "abcdef".substring(3,6);
System.out.print(s2 + "\t" + s4 + "\t");
System.out.println(s2 == s4); // 5
System.out.println(s2.equals(s4)); // 6
s3 = args[0];
System.out.print(s1 + "\t" + s3 + "\t");
System.out.println(s1 == s3); // 7
System.out.println(s1.equals(s3)); // 8
a--;
System.out.println(a + " " + b); // 9
}
}
output example:
附一个stackoverflow上关于引用的解释: http://stackoverflow.com/questions/1179937/how-does-a-c-reference-look-memory-wise
再次清楚的意识到自己的基础知识该有多差。。。细节决定成败啊!
还是要潜心钻研学术,低调求发展。。。
“勿在浮沙筑高台”
先上代码:
public class test {
public static void main(String[] args){
int a = 4;
int b = a;
String s1 = "abc";
String s2 = s1;
String s3 = "def";
String s4 = "def";
System.out.println(a == b); // 1
System.out.println(s1 == s2); // 2
System.out.println(s3 == s4); // 3
s3 = "abc";
System.out.println(s1 == s3); // 4
s2 = "abcdef".substring(3,6);
System.out.print(s2 + "\t" + s4 + "\t");
System.out.println(s2 == s4); // 5
System.out.println(s2.equals(s4)); // 6
s3 = args[0];
System.out.print(s1 + "\t" + s3 + "\t");
System.out.println(s1 == s3); // 7
System.out.println(s1.equals(s3)); // 8
a--;
System.out.println(a + " " + b); // 9
}
}
output example:
上面的结果能说明一些问题:
注释3处表示,s3,s4指向同一个内存地址,也就是说,“abc”这个字符串在编译时经过优化存放在全局数据区,并不会产生两个相同的string。
注释4处表示,s3 = "abc"这句是将s3指向"abc"这个字符串对象,而并不是修改s3原先指向的"def"处的内容。这证实了书上说的“string对象是不可变的”。
注释5、6处证明了 == 比较的是两个引用所指向对象的物理地址,而不是对象的值。用equals()比较值的结果是true,用 == 比较内存地址的结果是false,因为s2是从另一个字符串的返回的string子串,虽然内容和"abc"一样,但物理地址是不同的。
注释7、8处同理。从args传入参数。
(后来加上去的)注释9处,是验证b是否为a的引用。结果显示为 "3 4",说明a,b是独立的。同样的句式,用在string上和用在int上效果完全不一样,何解?因为int是基本类型,而String是一个类。
确实,很多时候我们潜意识里以为String和int都是基本类型,其实不然。
说到底,s2是s1的一个浅copy,不是深copy。
这里引用一段师兄的话
关于别名,其实就是编译器在编译时帮某个变相定义的另外一个名字而已,你就把引用当作在编译时编译器帮你默认写了个typedef。它其实不会产生额外内存开销(至少在C/C++里面是这样),只是编译的trick,按照我的理解,它应该仅存在于编译后产生的符号表里面,所以本身也没有地址。也就是说,一般情况下,引用(别名)本身是不占空间的,这和C++的定义是一致的。符号表是关键....
附一个stackoverflow上关于引用的解释: http://stackoverflow.com/questions/1179937/how-does-a-c-reference-look-memory-wise
再次清楚的意识到自己的基础知识该有多差。。。细节决定成败啊!
还是要潜心钻研学术,低调求发展。。。
“勿在浮沙筑高台”
2012年3月3日星期六
JAVA中的多态机制与类初始化
将一个方法调用同一个方法主体关联起来,称为绑定。
1.访问public类的main()函数 【static 方法】
2.加载并找出该类的编译代码(.class文件中)
3.加载过程中若存在基类(由extends可知)则先加载基类编译代码
4.根基类的static初始化,接着是导出类的static块
5.创建对象:所有类成员变量初始化(或置0);从根基类开始按顺序调用构造函数
6.实例变量按顺序初始化
关于成员函数在类中的表示方式,和师兄讨论过之后恍如大悟。他是站在C++的角度解释这个问题,但是我觉得很有道理。
首先,要清楚,函数并无“实例”的概念。程序被载入后,函数是在内存中的代码区而不是在数据区。函数不会被实例化,类在初始化生成this指针的时候,并不将任何函数装入内存,装入内存的是函数入口的偏移指针,这个入口指针可以一早确定并赋值给代表virtual function的类成员变量。
也就是说,s对象初始化的时候,f1(),f2()作为类成员变量(实际上是“指针变量”),其实存储的是对应函数的入口地址,而非函数代码本身。override之后,f1()接口处存放的就是派生类的函数入口,而非基类的函数入口。f2()由于定义为final,则其内容无法被修改,故依然指向基类的函数入口。
从上面的例子来看,circle从shape类继承来的是一个int,一个double,一个"指针变量",和一个"常指针";对象初始化的时候覆盖了f1()指针变量地址,然后创建了另一个指针变量f2()(扩展接口),所以就可以通过基类的构造器调用派生类的函数了(步骤5)。
在运行时根据对象的类型进行绑定称为后期绑定(动态绑定),这是实现多态的前提。
由多态引申出一个对象初始化具体步骤的问题,自己想了一个模型:
class Shape {
int a;
double b;
Shape(){f1();}
Shape(){f1();}
void f1(){println("shape.f1");}
private void f2(){println("shape.f2");}
}
public class Circle extends Shape {
void f1(){println("circle.f1");} //override
void f2(){println("circle.f2");}
public static void main(String[] args){
Shape s = new Circle();
s.f1();
s.f2();
}
}
output:
circle.f1
circle.f1
circle.f1
shape.f2
将派生类对象向上转型为基类,根据实际对象调用重载的方法,是多态的特性。开始我很疑惑,怎么可以在基类的构造器调用派生类的重载函数?在内存中,对象的具体实现是怎样的呢?就是以怎样的形式存在于内存空间中的呢?这就要涉及对象的初始化步骤:
2.加载并找出该类的编译代码(.class文件中)
3.加载过程中若存在基类(由extends可知)则先加载基类编译代码
4.根基类的static初始化,接着是导出类的static块
5.创建对象:所有类成员变量初始化(或置0);从根基类开始按顺序调用构造函数
6.实例变量按顺序初始化
关于成员函数在类中的表示方式,和师兄讨论过之后恍如大悟。他是站在C++的角度解释这个问题,但是我觉得很有道理。
首先,要清楚,函数并无“实例”的概念。程序被载入后,函数是在内存中的代码区而不是在数据区。函数不会被实例化,类在初始化生成this指针的时候,并不将任何函数装入内存,装入内存的是函数入口的偏移指针,这个入口指针可以一早确定并赋值给代表virtual function的类成员变量。
也就是说,s对象初始化的时候,f1(),f2()作为类成员变量(实际上是“指针变量”),其实存储的是对应函数的入口地址,而非函数代码本身。override之后,f1()接口处存放的就是派生类的函数入口,而非基类的函数入口。f2()由于定义为final,则其内容无法被修改,故依然指向基类的函数入口。
从上面的例子来看,circle从shape类继承来的是一个int,一个double,一个"指针变量",和一个"常指针";对象初始化的时候覆盖了f1()指针变量地址,然后创建了另一个指针变量f2()(扩展接口),所以就可以通过基类的构造器调用派生类的函数了(步骤5)。
2012年2月21日星期二
JAVA中访问权限控制的问题
记得在C++中,private部分主要是数据成员,public部分主要是函数成员,也就是类接口。如果没有特别声明public或者protect,private,也不会强调包访问权限这个概念。《thinking in JAVA》中的一些例子总是让人有眼前一亮的感觉。
现在想想,当时学的C++真是皮毛都算不上。囧rz
正题:
通过将构造函数声明为private,控制类外程序无法直接创建新类,而需要通过类接口来创建。书上说是可以通过特定函数(类接口)来实现特殊功能,比如控制数量。细心一想确实如此,要统计生成的对象数目,用一个计数器就可以了,但构造函数本身无法控制不生成对象,因为调用构造函数本身的同时就生成了一个对象。但是用函数成员的话只需要一个if就可以解决。
里面还提到一个设计模式的例子。private构造函数,类接口本身只提供一个类内生成对象的引用。称之为singleton(单例)。这种想法之前还真没接触过。。
example code:
/* Following the form of the example Lunch.java, create a class called
* ConnectionManager that manages a fixed array of Connection objects. The client
* programmer must not be able to explicitly create Connection objects, but can
* only get them via a static method in ConnectionManager. When ConnectionManager
* runs out of objects, it returns a null reference. Test the classes in main(). */
class connection{
private static int count = 0;
private connection(){System.out.println("connection created!");}
public static connection makecon(){
count ++;
return new connection();
}
public int howmany(){return count;}
}
public class CM{
private int num = 3;
connection[] con = new connection[3];
CM(){
for(connection x : con){
x = connection.makecon();
System.out.println(x.howmany());
}
}
public connection getcon(){
if(num != 0){
System.out.println("get " + (num - 1) + "th connection");
return con[--num];
}
else {
System.out.println("no connection left!");
return null;
}
}
public static void main(String[] args)
{
CM cm1 = new CM();
connection c1 = cm1.getcon();
connection c2 = cm1.getcon();
connection c3 = cm1.getcon();
connection c4 = cm1.getcon();
}
}
原书答案中,生成固定数组的foreach语句是用一个大括号包起来,而我将其放在构造函数中。之前还没出现过
{
for(...){}
}
这种用法,开始不明白,后来想起,类中只能存在数据成员和函数成员,不能出现这种函数语句的吧?。。。C++中反正我是没见过 = =
现在想想,当时学的C++真是皮毛都算不上。囧rz
正题:
通过将构造函数声明为private,控制类外程序无法直接创建新类,而需要通过类接口来创建。书上说是可以通过特定函数(类接口)来实现特殊功能,比如控制数量。细心一想确实如此,要统计生成的对象数目,用一个计数器就可以了,但构造函数本身无法控制不生成对象,因为调用构造函数本身的同时就生成了一个对象。但是用函数成员的话只需要一个if就可以解决。
里面还提到一个设计模式的例子。private构造函数,类接口本身只提供一个类内生成对象的引用。称之为singleton(单例)。这种想法之前还真没接触过。。
example code:
/* Following the form of the example Lunch.java, create a class called
* ConnectionManager that manages a fixed array of Connection objects. The client
* programmer must not be able to explicitly create Connection objects, but can
* only get them via a static method in ConnectionManager. When ConnectionManager
* runs out of objects, it returns a null reference. Test the classes in main(). */
class connection{
private static int count = 0;
private connection(){System.out.println("connection created!");}
public static connection makecon(){
count ++;
return new connection();
}
public int howmany(){return count;}
}
public class CM{
private int num = 3;
connection[] con = new connection[3];
CM(){
for(connection x : con){
x = connection.makecon();
System.out.println(x.howmany());
}
}
public connection getcon(){
if(num != 0){
System.out.println("get " + (num - 1) + "th connection");
return con[--num];
}
else {
System.out.println("no connection left!");
return null;
}
}
public static void main(String[] args)
{
CM cm1 = new CM();
connection c1 = cm1.getcon();
connection c2 = cm1.getcon();
connection c3 = cm1.getcon();
connection c4 = cm1.getcon();
}
}
原书答案中,生成固定数组的foreach语句是用一个大括号包起来,而我将其放在构造函数中。之前还没出现过
{
for(...){}
}
这种用法,开始不明白,后来想起,类中只能存在数据成员和函数成员,不能出现这种函数语句的吧?。。。C++中反正我是没见过 = =
2012年2月18日星期六
关于import & package的应用与疑问
在《thinking in JAVA》第六章 访问控制权限 中,说到了package的用法。对于解决像System.out.print这种冗长的输出格式,也不失为一种好的解决办法。但是建立自己的类库,代码的可移植性也就要差点了。当然这个package也是鼓捣了好一阵才弄明白怎么用,囧
首先涉及到classpath的设置。安装JDK的时候会有设置classpath这一步。比如,classpath中存在“%java_home%\lib;"
import access.test.*
就会在classpath下寻找access]test 这个目录中的java源文件和class文件(也就是%java_home%\lib\access\test\ 这个目录)
个人感觉,import 和C++中的 using namespace 比较类似,反而不是属于include那一类。简单的说就是命名空间的划分引用。
也就是说,一般在类库中的文件,源代码开头(除注释)都是package 语句,用以将该java文件和class文件划分在指定的命名空间中。调用的时候就通过import语句导出。也就是说类库中java源文件中的package路径就是文件本身的路径?
其实一开始让我疑惑的是,一般我们会
import java.util.*
但是在我的classpath
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
搜寻路径中,没有java这个目录,而java\util\ 这个目录实际上存在于%JAVA_HOME%\src.zip 这个文件中打包保存。这个怎么解释呢?应该是JAVA路径支持压缩包
比如:
CLASSPATH:也指定一个路径列表,是用于搜索 Java 编译或者运行时需要用到的类。在 CLASSPATH 列表中除了可以包含路径外,还可以包含 .jar 文件。Java 查找类时会把这个 .jar 文件当作一个目录来进行查找。通常,我们需要把 JDK 安装路径下的 jre/lib/rt.jar (Linux: jre/lib/rt.jar) 包含在 CLASSPATH 中。
首先涉及到classpath的设置。安装JDK的时候会有设置classpath这一步。比如,classpath中存在“%java_home%\lib;"
import access.test.*
就会在classpath下寻找access]test 这个目录中的java源文件和class文件(也就是%java_home%\lib\access\test\ 这个目录)
个人感觉,import 和C++中的 using namespace 比较类似,反而不是属于include那一类。简单的说就是命名空间的划分引用。
也就是说,一般在类库中的文件,源代码开头(除注释)都是package 语句,用以将该java文件和class文件划分在指定的命名空间中。调用的时候就通过import语句导出。也就是说类库中java源文件中的package路径就是文件本身的路径?
其实一开始让我疑惑的是,一般我们会
import java.util.*
但是在我的classpath
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
搜寻路径中,没有java这个目录,而java\util\ 这个目录实际上存在于%JAVA_HOME%\src.zip 这个文件中打包保存。这个怎么解释呢?应该是JAVA路径支持压缩包
比如:
CLASSPATH:也指定一个路径列表,是用于搜索 Java 编译或者运行时需要用到的类。在 CLASSPATH 列表中除了可以包含路径外,还可以包含 .jar 文件。Java 查找类时会把这个 .jar 文件当作一个目录来进行查找。通常,我们需要把 JDK 安装路径下的 jre/lib/rt.jar (Linux: jre/lib/rt.jar) 包含在 CLASSPATH 中。
订阅:
博文 (Atom)
