在最后一节课的末尾,吴老师带着我们做了一个简单的回顾。对这两周课程整体进行了总结。
一、课程核心要义——Java面向对象编程的四个基本特征
1.封装
主要是把过程和数据包围起来,不对外部公开内部的数据和逻辑,从而保护内部的数据结构不被外界改变,起到保护作用!
主要通过对数据进行类型定义private,public,protected来对数据的类型进行区分,让各种类型的数据的使用范围有不同之处。
访问位置 | private | protected | public |
本类内部 | 可见 | 可见 | 可见 |
同包的其他类或者子类 | 不可见 | 可见 | 可见 |
其他包 | 不可见 | 不可见 | 可见 |
PS:在以后的程序之中所有类之中的成员变量都声明为private,然后在类里面添加多个get方法(返回值为想要查看的成员变量)以防用户对成员变量私自进行修改。
举例如下(以第四次作业中的二元组<String,Integer>组成的类pair为例)
public class Pair { private String word; private int times; public Pair(String str,int num){ word=str; times=num; } public String getWord(){ return word; } public int getTimes(){//将time封装起来 return times; } }
2.抽象
抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。例如,看到一只蚂蚁和大象,你能够想象出它们的相同之处,那就是抽象。抽象包括行为抽象和状态抽象两个方面。例如,定义一个Person类,如下:
class Person{ String name; int age; }
忽略主题全部不打算把全部事件描述下来,只是抽取主要部分抽象化描述,可以理解抽象是一个接口类或一个抽象类!比如:描述一个抽象功能,用接口表示,只要添加、删除、修改等方法功能!(抽象类和接口类是Java抽象的一个机制)!
抽象类(abstract class): 1.可以实现继承父类 2.可以拥有私有的方法或私有的变量, 3.只能单独继续一个类!
接口类(interface): 1.不可以实现继承 2.不可以拥有私有的方法或私有的变量 3.一个接口类可以实现多重继承(比如A类接口实现B\C\类,那么B\C\继承是另一个类)!
接口是为了弥补Java单继承问题。
接口使用的例子(以第二次上课的Box为例):
//接口的定义 public interface Geometry { public double getVolume(); public String toString(); }
//类加到接口上时要用implements关键字 public class Box implements Geometry{ private double width; private double length; private double height; private double scale; private double vol; public Box(double w, double l, double h,double s){ width = w; length = l; height = h; scale = s; vol = volume(); } private double volume() { // TODO Auto-generated method stub return width*scale*height*scale*length*scale; } public double getVolume(){ return vol; } public String toString(){ return "Volume of Box:"+vol; } }
使用的方法:(直接就抽象起来了,不再区分时box还是cylinder)
Vector<Geometry> list; for(int i=0;i<list.size();i++){ res+=list.get(i).getVolume(); }
3、继承
(1)在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。
(2)通过 extends 关键字让类与类之间产生继承关系。
class SubDemo extends Demo{
} //SubDemo是子类,Demo是父类
(3)继承的特点
只支持单类继承,不支持多个类继承!!!!
//一个类只能有一个父类,不可以有多个父类。 class SubDemo extends Demo{} //ok class SubDemo extends Demo1,Demo2...//error
Java支持多层(重)继承(继承体系)。
1
2
3
|
class
A{}
class
B
extends
A{}
class
C
extends
B{}
|
(4)super的使用
super是一个关键字,代表父类的存储空间标识。(可以理解为父亲的引用)
super和this的用法相似。
this代表对象的引用(谁调用就代表谁);
super代表当前子类对父类的引用。
使用场景
-
- 当子父类出现同名成员时,可以用super进行区分;
- 子类要调用父类构造函数时,可以使用super语句。
(5)方法的重写与覆盖(以课上的CharSet为例)
public class NewCharSet extends CharSet{ int service=0; public NewCharSet(char[] charSet){//构造方法的重写,下面不需要写super.CharSet super(charSet); } public void insert(int index, char alpha){//其余方法的重写都是super.方法名 this.service++; super.myInsert(index, alpha); } }
4.多态(没有重点讲,以下资料来源于网络)
不同类的对象对同一个类的对象做出不同的响应信息!(Java提出多态性是对Java单继承的一个补充)。
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态性增强了软件的灵活性和扩展性。例如,下面代码中的UserDao是一个接口,它定义引用变量userDao指向的实例对象由daofactory.getDao()在执行的时候返回,有时候指向的是UserJdbcDao这个实现,有时候指向的是UserHibernateDao这个实现,这样,不用修改源代码,就可以改变userDao指向的具体类实现,从而导致userDao.insertUser()方法调用的具体代码也随之改变,即有时候调用的是UserJdbcDao的insertUser方法,有时候调用的是UserHibernateDao的insertUser方法:
UserDao userDao=daofactory.getDao();
userDao.insertUser(user);
比喻:人吃饭,你看到的是左手,还是右手?
二、有关hashmap和String类(正则表达式)
在这两周的学习之中,给我留下在深刻印象的就是String类和hashmap这两个结构和类库了。
正则表达式在处理字符串的时候是一大神器!!
有关这三个知识点的总结参见我写的前几篇博客。
三、以第三、四次作业为例来分析优化程序的过程
1.第三次作业(词频统计)
A.刚开始看到这个题目的的时候,由于恰好接触了hashmap,因而,就想到直接使用hashmap来实现这个功能。具体做法是将字符串作为key键值,将该单词在文章之中出现的所有位置都存到一个ArrayList之中,再将该list作为value值存储到hashmap之中。通过使用split函数将文章之中的所有单词都提取出来,然后一个个对hashmap里的值进行更新。
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class TokenManager { HashMap<String,ArrayList<Location>> dictionary; public class Location{ public int row; public int column; public Location(int a,int b){ row=a; column=b; } } public TokenManager(String filename)throws IOException { dictionary=new HashMap<String,ArrayList<Location>> (); BufferedReader reader=new BufferedReader(new FileReader(filename)); String str; int row=-1; while((str = reader.readLine()) != null){ row++; String[] strSet=str.split("\\W+|_+"); //找到使用split方法后得到的逐个单词的位置 for(int i=0,index=-1;i<strSet.length;i++){ if(!strSet[i].equals("")){ index=str.indexOf(strSet[i], index); if(!dictionary.containsKey(strSet[i])){ ArrayList<Location> list=new ArrayList<Location>(); dictionary.put(strSet[i], list); } ArrayList<Location> list=dictionary.get(strSet[i]); Location pair=new Location(row,index); list.add(pair); dictionary.put(strSet[i], list); index+=strSet[i].length(); } } } reader.close(); } public void output(String filename)throws IOException{ Object[] key_list=dictionary.keySet().toArray(); ArrayList<String> word_list=new ArrayList<String>(); for(int i=0;i<key_list.length;i++){ word_list.add((String)key_list[i]); } Collections.sort(word_list); String out=""; File fileout=new File(filename); FileWriter writer=new FileWriter(fileout,true); for(int i=0;i<word_list.size();i++){ String word=word_list.get(i); ArrayList<Location> list=dictionary.get(word); int repeat=list.size(); out=word+": "+repeat+" : {"; writer.write(out); for(int j=0;j<repeat-1;j++){ Location loca=list.get(j); out="("+loca.row+"," +loca.column+"),"; writer.write(out); } out="("+list.get(repeat-1).row+"," +list.get(repeat-1).column+")}\r\n"; writer.write(out); } writer.close(); } }
这是最终优化结果之后的代码,最后总用时是2.5s左右。
在第三次作业之中发现较大的优化之处就是有关文件的读入以及最后向文件之中写入大量的数据所花费的时间。
(1)文件的读入方式(尽量使用BufferedReader)
最开始的时候进行的读入尝试,是使用类似C语言之中的getchar()函数,字符一个一个进行读入,但是最后的结果却是十分不理想,运行时间接近于200s左右,在查阅资料之后尝试更改成了使用BufferedReader来读入,速度与效率有了极大的提高。
基本的BufferedReader的用法如下图所示:
BufferedReader reader=new BufferedReader(new FileReader(filename));
PS:BufferedReader在使用过程之中想比较filereader有了较大的功能进步,比如filereader之中是不支持readLine()这个函数的(不可以逐行读入进行处理),但是BufferedReader之中可以使用readerLine这个方法实现逐行读入处理。
(2)向文件之中写入大量的数据
教训:
!!!flush不要胡乱用。(注意,直接写writer.write()你会发现有些时候是写不进去的,因为可能一直在缓存区之中,当使用flush时可以将缓存区的内容写进去,或者在关闭文件的时候回将先前写入缓存区的内容一次性写入目标文件之中)。
!!!向文件之中写数据的时候不要用把所有的数据用字符串拼接全部连接到一起再一次性全部输出,一来拼接过程要创建新的对象而且要多次遍历因而效率降低明显,二来将过量的数据写到文件之中也会导致输出效率骤然变慢。
上述最终改好的程序是每次调用函数打开文件然后在函数的末尾将文件关闭,在最开始的时候 ,关键的输出语句是采用
writer.flush();
但是相同的算法运行时间几乎是100倍左右,因此可以清晰地看到,在向指定文件之中写入大量数据时,一定不要过多使用flush语句。
B.在采用上面的做法之后,我对所用的结构进行了分析,经过理论分析可以知道该算法的时间复杂度为O(nlogn),主要的复杂度都集中在排序那里。后来想到数据结构中曾经用到过的字典树这一数据结构,这种数据结构的理论时间复杂度为O(kn),k为字符集之中所含的字符总数,应该来说是理论最优算法,于是开始就按照字典树的算法理论写了一份新的代码。
package homework3; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; public class TokenManager{ public void close() throws IOException{ writer.close(); } FileWriter writer; int time; public ArrayList<TokenString> wordlist; public Node root; public TokenManager() throws IOException{ time=0; root=new Node(0); wordlist=new ArrayList<TokenString>(); writer=new FileWriter("c:\\Users\\mly\\Desktop\\output.txt",true); } public class Node{// public char alpha; public int end_flag; public Node[] next; public ArrayList<Integer> location; public Node(int k){ alpha=(char)k; end_flag=0; location=new ArrayList<Integer>(); next=new Node[128];//题目中含数字大写字母所以要求更多 for(int i=0;i<next.length;i++){ next[i]=null; } } } public void insert(String word,int index){ Node p=root; for(int i=0;i<word.length();i++){ if(p.next[(int)word.charAt(i)]==null){ p.next[(int)word.charAt(i)]=new Node((int)word.charAt(i)); } p=p.next[(int)word.charAt(i)]; if(i==word.length()-1){ p.location.add(index); if(p.end_flag==0){ p.end_flag=1; } } } } public void dfs(Node p,String str) throws IOException{//使用递归的方法来实现字典树的的遍历 if(p!=null){ if(p.end_flag==1){ //System.out.println(str.substring(1)+p.alpha+": "+p.location.size()); //System.out.println(p.location); output(str.substring(1)+p.alpha+": "+p.location.size()+p.location); TokenString wordNode=new TokenString(str.substring(1)+p.alpha,p.location); wordlist.add(wordNode); } for(int i=0;i<root.next.length;i++){ str+=p.alpha; dfs(p.next[i],str); str=str.substring(0, str.length()-1); } } } public void output(String str)throws IOException{ long startTime=System.currentTimeMillis(); this.writer.write(str+"\r\n"); long endTime1=System.currentTimeMillis(); time+=endTime1-startTime; } }
最终的程序运行结果如下图所示:
我们可以看到虽然这种算法理论上速度会快很多,但是实际上由于种种因素导致执行起来并没有预想之中那么高效。
2.从第四次作业(短语词频分析来看理论最优与实际时间的关系问题)
在上面第三次作业优化分析的过程之中我看到了理论最优的算法在写出来之后的时间效率未必是最优的。在第四次作业之中,要求输出频率最高的五个短语。在以前接触过TOP K问题的 算法,我觉得如果把所有的数据均进行排序会降低程序的效率,于是把程序按照hashmap的value值排序(使用的是系统自带的sort函数)改成了类似使用top k算法(k=5,就没有写堆,就简单地依次遍历)
ArrayList<Integer> numListTop5=new ArrayList<Integer>(); ArrayList<String> wordListTop5=new ArrayList<String>(); Iterator iter = temp.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); String key = (String)entry.getKey(); int val = (int)entry.getValue(); sum+=val; int min=val,minIndex=-1; if(numListTop5.size()<5 ){ numListTop5.add(val); wordListTop5.add(key); } else{ for(int i=0;i<5;i++){ if(min>numListTop5.get(i)){ min=numListTop5.get(i); minIndex=i; } } if(minIndex!=-1){ numListTop5.set(minIndex,val); wordListTop5.set(minIndex,key); } } } int len=numListTop5.size(); if(len<5){ System.out.println("Only "+len+" phrase formed by "+word); for(int i=0;i<len;i++){ System.out.println(word+" "+wordListTop5.get(i)+numListTop5.get(i)); } } else{ for(int i=0;i<5;i++){ System.out.println(word+" "+wordListTop5.get(i)+numListTop5.get(i)); } }
采用的从头到尾依次遍历的方法,但是最后的优化结果比较差,没有减少时间,反而使运行时间越来越长。
3.总结如何提高程序运行效率
在第四次课堂上,吴老师就通过一个slowJava的例子向我们展示了如何比较方便快捷的通过优化部分模块来提高程序运行时间。
首先,最重要的一部是要将已写好的程序进行分块,然后再每块都设置时间显示器,在程序最后打印出来各个模块所用的时间大小。(只有先了解程序各部分运行情况,才能对症下药,优化到最大程度)
其次,找到占用程序运行大时间的代码部分,想办法从中进行优化和改进 。
在改进的过程中,第一考虑在算法层面上的时间复杂度,第二重点考虑输入输出部分,第三考虑如果实现同一个目的有不同的方法,将不同的方法都实现一次记录一下时间,找到最优的程序组合。
在编程过程尽量考虑使用Java类库里面的方法,理论最优的算法实际跑出来的的效果可能与理想之中的时间有所偏差。
四、课程感想以及建议
在选这门课的时候,是有些犹豫的,因为感觉在这里因为要上四天的课要多待2周。等到课程开始的时候才发现,四天的课程是不足以让我们把Java学会学透,那空余的时间是用来做作业以及自己查阅资料用的。这门课上吴老师的讲解已经不再像我们C语言那样,花大量的时间在语法的讲解上,而是将侧重点放在了利用这有限的时间花更多的精力来让我们去深入理解面向对象的核心思想。老师给我们引导出了一种全新的编程语言学习模式,这颠覆了我们在学习c语言过程之中老师花大量的时间来教授语法知识。就是在遇到一门全新的语言时,先基本了解最基础的语法框架,然后在具体任务(实例)之中不断查询自己所需要的语法知识,这种学习方式无疑比直接机械地讲解语法更加高效,掌握语言的也会更加熟练。
这门课除了教会我一门新的编程语言,还教会我了一个道理——在实际软件编写过程之中一定要考虑到异常的处理毕竟以后软件的客户很有可能会提供一些非法输入,这些时候就要考虑程序不能总是直接退掉,而是想办法处理,或着给出体提示信息。(毕竟以后的工作之中不会所有的输入数据都会像C语言作业那样有严格的范围以及保证绝对的合法性与正确性)。
建议:在这门课之中老师的课程安排清晰合理,唯一感觉难受的地方就是作业,可能大部分原因还是出在自己身上,可能是本学期C语言的作业有严格的测评系统来规范,所以在做Java作业的时候经常会对说明文档的意思读不懂,题意经常弄不是很懂,而且作业要求之中没有很严格限定输入输出形式,在做作业的时候经常陷入惯性思维在输入输出形式之上纠结许久。我认为老师可以在以后开这门课的时候可以考虑尝试一下搭建一个简易的Oj平台测评。