太简单了,直接略过。
我选择使用了JetBrainde IDEA社区版,直接忽略
主要关心的是boolean类型,包含false和true,与C++是同一个类型的
合法特殊字符为任何代表字母的Unicode字符,但是不能出现+
(操作符)和copyright(除字母外的其他Unicode字符)。其中$
尽量不要在自己的代码中使用,一般出现在Java编译器或者其他工具生成的名字中。
需要显示初始化变量,与C++一致。
在java中,使用final
指示常量,如final double CM_PER_INC=2.54
。final表示该变量只能赋值一次,一旦赋值就不能修改,习惯上常量名使用大写。
类常量可以使用static final
进行设定,在某个类内部定义
public class test{
public static final double CM_PER_INC=2.54;
}
求余符号为%
double x = 9,997;
int nx = (int) x;
任何语言的字符串都是值得重视的。
java使用类型String
定义字符串
String greeting = "Hello";
String s = greeting.substring(0,3);//Hel 前闭后开区间,对于substring(a,b),子串长度为b-a
String t = "123"+"456";//123456
String类不能直接修改字符串,只能够使用子串+拼接的方式进行间接修改。
原理是编译器让所有的字符串共享,可以想象字符串被放在公用的存储池中,赋值字符串后,新字符串与原字符串指向相同的对象。
可以使用s.equals(t)
检测是否相等,但不能使用==
,后者只能确定两个字符串是否放在同一个位置上,这点C++应该也是一样的。
可以调用string.length()
返回长度
可以调用string.charAt(index)
返回指定位置的char类型变量。
……不用想着背了,肯定是用到的时候再查的。
IO也是很重要的
标准输出流:System.out.println
标准输入流:
import java.util.*;//Scanner类定义在java.util包中
Scanner in = new Scanner(System.in);//Scanner绑定标准输入流
String nextline = in.nextLine();//读取下一行
String nextword = in.next();//读取下一个单词
int age = in.nextInt();//读取整数
格式化输出:System.out.printf("%8.2f",x);
,同样,与C++类似
也是一般最常用的
Scanner in = new Scanner(Paths.get("myfile.txt"));//读取文件
PrintWriter out = new PrintWriter("myfile.txt");//写文件
相对路径为启动环境的根目录。使用集成开发环境时,路径地址可以使用String dir = System.getProperty("user.dir");
获得
block,指花括号括起来的若干条简单java语句
java支持带标签的break来跳出多重循环,标签必须放在希望跳出的最外层循环之前,加上引号
read_data:
while(...){
for(...){
break read_data;//直接跳到指定循环之外
}
}
java.math中的BigInteger和BigDecimal,可以处理任意长度数字序列的树枝。前者为任意精度整数运算,后者为任意精度浮点数运算。可以使用valueOf方法将普通数值转换为大数值。
int[] a = new int[100]
,声明数组
匿名数组smallPrimes = new int[]{1,2,3,4,5}
,可以在不创建新变量的情况下重新初始化一个数组
数组拷贝int[] luckyNumbers = smallPrimes;
为引用,想要拷贝数值应该使用Arrays.copyOf
方法(可以使用该方法来增加数组长度)
数组排序Arrays.sort(arr)
double[][] balances = new double[YEARS][RATES]
Java本质上没有多维数组,只有一维数组。因此,二维数组的每一行可以拥有不同的长度
int[][] odds = new int[NMAX+1][]
for(int n=0;n<=NMAX;n++){
odds[n] = new int[n+1];
}
类之间的关系:
空对象null
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendasr.MONTH);//访问器
deadline.set(Calendar.YEAR,2001);//更改器
对实例域作出修改的访问成为更改器方法,仅访问实例域的方法称为访问器方法。
类方法构成
public String getName()
其中public表示访问控制,String表示返回值,函数名内部为形参表
java编译器可以认为内置了make功能,就算使用java XXX.java
命令没有显示编译其他的java文件,它也会查找其他的java文件。
一般建议实例域采用private来维持封装
C++中的构造函数,没有看到有什么不同的
PS:Java中的所有对象都是在堆中构造的,容易遗漏new
操作符
PPS:不要在构造器中定义与实例域重名的局部变量,会重复。
即对于类方法
public void raiseSalary(double byPercent){
double raise = salary*byPercent/10;
salary += raise;
//一般推荐this.salary += raise;,这样可以显式区分局部变量和实例域
}
java中可以选择显示调用this指针,也可以不调用。这里类方法中的第一个参数为隐式参数,即类自己。该类方法的副作用就是salary会一起改变。
方法可以访问所有类的私有域(与C++类似,抱歉我C++学的不好)
class Employee{
private String name;
public boolean equals(Employee other){
return name.equals(other.name);
}
}
对象中的变量一般是跟着对象走的,但是static的变量可以看作独立于具体对象之外。这样,对于所有的对象,它们共享同样的静态域。
静态方法是一种不能向对象实施操作的方法,可以认为静态方法没有this参数。
可以使用静态方法来实现工厂函数。
一般来说,存在按值调用和按引用调用。Java总是默认采用按值调用,但是需要注意,=
赋值号一般总是直接复制对象的地址,除非使用clone
这也就是说,方法得到的是所有参数值的一个拷贝。但是如果参数是自定类的话,则拷贝的内容为类的地址,因此可以认为是引用传值。
即构造函数重载,相同的构造函数可以使用相同的名字、不同的参数。
关键字this引用方法的隐式参数
public Employee(double s){
//call Employee(String,double)
this("Employee #"+nextId,s);
nextid++;
}
这样对于公共的构造器代码,只用编写一次即可。
为类添加finalize方法,将在垃圾回收器清除对象之前调用。
PS:在实际应用中不要依赖finalize方法,因为实际很难知道具体什么时间会调用这个函数
Java允许使用包将类组织起来,有点类似C++中的namespace
导入的方式,一种是输入全名java.util.Date today = new java.util.Date();
也可以使用import java.util.*;
导入全部类,这样就不用写入全名了。
另外,*
只能导入一个包,不能使用java.*
的方式导入所有包。
import static java.lang.System.*
可以导入静态方法和静态域。
想要将类放入包中,就必须将包的名字放在源文件的开头,可以必须写全名,例如
package com.horstmann.corejava;
,而不能只是package corejava
同时,包中的文件需要被放置在与完整的包名匹配的子目录中,例如上面的包应该被放在com/horstmann/corejava
下。
对于private定义的类,只有同一个包能够访问。而public类则是导入包即可见。
主要是使用JAR文件。一般将JAR文件放在一个目录中,然后设置类路径classpath(一般是java -classpath/-cp XXX
),就可以读取。在UNIX环境中,类路径的不同项目之间采用冒号分割。也可以设置环境变量CLASSPATH
JDK的工具,javadoc,可以由源文件生成一个HTML文档。
有点类似python的doc,不过是放在定义的前面而不是后面,并且对于/**
开头的注释来说,每一行的开头都要有*
方法注释还可以使用以下的标记:
@param
变量描述@return
返回描述@throws
类描述通用注释:
@author
姓名@version
版本文本@since
对引入特性的版本描述执行命令javadoc -d docDirectory nameOfPackage
,即可生成docDirectory
下的指定包的HTML文件。
可以使用关键词extends
表示继承,且JAVA中只有公有继承,没有C++中的私有继承和保护继承
一些显然但容易忘的事实:子类方法并不能直接访问超类的私有域,必须借助于公有接口。如果需要调用超类的同名方法,应该使用特定关键字super
同时,super可以在构造器中使用,比如
public Manager(String test){
super(test);
}
super与this指针具有类似之处,它们都能调用方法和构造器。但是super是不能赋值的,它只能指示编译器。
比如Manager继承了Employee,显然Employee变量即可以是Employee对象,也可以是Manager对象或者其他继承对象。
多态的特征依赖于编译器调用对象方法的执行过程:
不允许扩展的类称为final类
类中特定方法也可以声明为final,这样子类就不能覆盖这个方法。(final类中的所有方法自动称为final方法)
这样做的意义是为了保证它们在子类中不会改变语义。
Manager boss = (Manager) staff[0];
,其中staff可能是继承类数组。如果staff中是超类,则会报错。
可以使用
if (staff[1] instanceof Manager){
boss = (Manager) staff[1]
}
确保staff中存储的是Manager或其子类。
PS:null instanceof C
不会产生异常,只会返回false。
对于上层的通用类,可能设计成抽象类会更好。
abstract class Person{
public abstract String getDescription()
}
抽象类可以不用实现具体方法,但是可以包含具体数据和具体方法。
PS:很多人认为,在抽象类中包含具体方法是有害的。、
如果希望超类中的某些部分被子类访问,应该设为protected
而非private
。
但是这样只能访问自己对象的超类中的指定部分,而不能访问其他对象的超类中的指定部分。这与private
还是有一定区别的。
getClass().getName()
获得类名的字符串。Java允许在运行时确定数组的大小。
int actualSize = ...;
Employee[] staff = new Employee[actualSize];
当然,这样子也无法动态更改数组大小。因此,一般使用ArrayList类来进行实现。该类类似于C++中的vector
ArrayList<Employee> staff = new ArrayList<Employee>();
ArrayList只能使用get和set来访问数组元素。
比如尖括号内的类型不能是基础类型,所以必须写成ArrayList<Integer> list = new ArrayList<>();
此时list.add(3);
会自动变换成list.add(Integer.valueOf(3));
该过程成为自动装箱autoboxing,或者autowrapping
例如System.out.printf
的实现public PrintStream printf(String fmt,Object... args){return format(fmt,args);}
其中...
打死表这个方法可以接收任意数量的对象,后者args是一个Object[]
数组。
public enum Size{SMALL,MEDIUM,LARGE};
能够分析类能力的程序称为反射
Object类中的getClass()方法可以返回一个Class类型的实例。虚拟机为每个类型管理一个Class对象,可以使用==运算符进行比较,比如
if (e.getClass() == Employee.class)
以及使用newInstance()来快速地创建一个类的实例e.getClass().newInstance()
,可以创建一个与e具有相同类类型的实例,调用默认的构造器。
String s = "java.util.Date";
Object m = Class.forName(s).newInstance();//forName根据类名进行查找,所以创建了一个java.util.Date类型的变量
可以使用Constructor.newInstance(Object[] args)
来调用指定的构造器
try{
}catch(Exception e){
e.printStackTrace();
}
反射机制最重要的内容——检查类的结构
java.lang.reflect包中有三个类:
Field有getType方法,返回描述域所属类型的Class对象。
Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getClass();//拿到Employee对应的class对象
Field f = cl.getDeclaredField("name");//拿到Employee class的name作用域
Object v = f.get(harry);//harry的name field的具体值,即Harry Hacker
该代码有一个问题,name是私有域,因此会爆出错误。只有利用get方法才能得到可访问域的值。除非拥有访问权限,不然只能查看任意对象有哪些域,而不允许读取它们的值。
可以调用setAccessible
方法来覆盖访问控制,这样就可以访问私有域了。
另一个问题是get方法返回的是Object,因此如果返回值是double的时候会有问题。此时应该使用getDouble方法,反射机制会自动打包。
泛用的toString()例子
package objectAnalyzer;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class ObjectAnalyzer{
private ArrayList<Object> visited = new ArrayList<>();
/**
* 将一个对象转为列举所有field的数组
* @param obj an object
* @return a string with the object's class name and all field names and value
*/
public String toString(Object obj){
if (obj == null) return "null";
if (visited.contains(obj)) return "...";
visited.add(obj);
Class cl = obj.getClass();
if (cl==String.class) return (String) obj;
if (cl.isArray()){
String r = cl.getComponentType() + "[]{";
for(int i = 0;i<Array.getLegnth(obj);i++){
if(i>0) r+=",";
Object val = Array.get(obj,i);
if(cl.getComponentType().isPrimitive()) r += val;
else r += toString(val);
}
return r + "}";
}
String r = cl.getName();
//inspect the fields of this class and all superclasses
do{
r += "[";
Field[] fields = cl.getDeclaredFields();
AccessibleObject.setAccessible(fields,true);
//get the names and values of all fields
for (Field f: fields){
if(!Modifier.isStatic(f.getModifiers())){
if(!r.endsWith("[")) r += ",";
r += f.getName() + "=";
try{
Class t = f.getType();
Object val = f.get(obj);
if(t.isPrimitive()) r += val;
else r += toString(val);
}
catch(Exception e){
e.printStackTrace();
}
}
}
r += "]";
cl = cl.getSuperclass();
}
while(cl != null);
return r;
}
}
比如制作一个通用的Arrays.copyOf(array,legnth)
,这个方法可以用来扩展已经填满的数组。
第一次尝试:
public static Object[] badCopyOf(Object[] a,int newLength){
Object[] newArray = new Object[newLength];
System.arrayCopy(a,0,newArray,0,Math.min(a.legnth,newLength));
return newArray;
}
问题在于,该函数返回的是对象数组Object[],这是基类数组。显然,将基类数组赋值给子类数组会报错。
为了编写通用数组代码,就必须创造与原数组类型相同的新数组,即要用到反射类。最关键的就是java.lang.reflect.Array.newInstance
静态方法,它能够构造新数组,调用它时必须提供数组的元素类型和数组的长度。
public static Object goodCopyOf(Object a,int newLength){
Class cl = a.getClass();//拿到对应的class对象
if(!cl.isArray()) return null;
Class componentType = cl.getComponentType();//拿到类型
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType,newLength);//创建数组
System.arraycopy(a,0,newArray,0,Math.min(length,newLength));
return newArray;
}
此时,CopyOf代码可以扩展任意数组,而不只是对象数组。这也是为什么将a设置为Object
类型而非Object[]
类型。
表明上看,java没有方法指针,设计者认为接口是更好的方案。但是反射机制运行用户调用任何方法。
Field类的get方法查看对象域,而Method类有一个invoke方法,允许调用包装在当前Method对象中的方法。
Object invoke(Object obj,Object... args)
,第一个参数是隐式参数,其余对象提供显式参数。
对于静态方法,第一个参数可以忽略,设置为null
假设m1为Employee类的getName方法(String getName(void)),则String n = (String) m1.invoke(harry);
就直接调用了这个方法。(PS:harry为对应的Employee对象)
得到Method对象可以使用Method getMethod(String name,Class.. parameterTypes)
,根据方法的名称和参数类型来得到想要的方法,例如Method m2 = Employee.class.getMethod("raiseSalary",double.class);
接口不是类,而是对类的一组需求描述,类要遵从接口描述的统一格式进行定义
例如:
public interface Comparable{
int compareTo(Object other);
}
即要求任何实现Comparable接口的类都需要包含compareTo方法,并且具体事项都是符合的。
接口中的所有方法自动地属于public,因此不必提供关键字public
为了让类实现接口,一般包含两个步骤:
implements interfaceName
PS:虽然接口并没有把方法声明为public,但是实现接口时,必须把方法声明为public。否则,编译器将认为这个方法的访问属性是包可见性,即类的默认访问属性,之后编译器就会给出试图提供更弱的访问权限的警告信息。
接口不是类,不能进行实例化,比如new
等。到那时可以声明接口的变量,例如Comparable x
。接口变量必须引用实现了接口的类对象,有点类似于严格限定的抽象基类。
类似地,也可以使用instanceof
检查一个对象是否实现了某个特定的接口。anObject instanceof Comparable
接口之间可以继承:
public interface Movable{
void move(double x,double y);
}
public interface Powered extends Movable{
double milsPerGallon();
}
接口不能包含实例域或静态方法,但是可以包含常量:
public interface Powered extends Movable{
double milsPerGallon();
double SPEED_LIMIT = 95; // a public static final constant
}
接口中的域将被自动设为public static final
每个类只能有一个超类,但是可以实现多个接口.
使用抽象类表示通用属性时会存在一个问题:每个类只能扩展于一个类,这使得多个通用属性不能共存。而多个接口可以共同实现。
调用clone()
执行复制而不是引用。但需要注意,clone方法对属性中的自定义类型只能进行shallow copy。
而且clone方法是protected的,意味着类只能克隆自己。
callback是一种常见的程序设计模式,一般我在js见的比较多,或者说C++中的函数指针,在某个事件发生后,直接调用指定的这个可变的函数。
在java中,传递的是一个实现了指定接口的对象。例子就不举了
即定义在另一个类中的类。内部类可以访问该类定义所在的作用域中的所有数据,并相对于同一个包隐藏起来。
当想要使用一个回调函数而又不想编写过多代码时,可以使用匿名内部类。
C++使用的是嵌套类。嵌套时类之间的关系而并不是对象之间的关系。对于一个嵌套类,可能并不会实现嵌套内的类。而内部类中里面的类会有一个隐式引用,指向实例化该内部对象的外围类对象,因此会很有意思。
static内部类则没有这种附加指针,与C++的嵌套类类似。
内部类可以隐式地访问创建它的外部对象,并使用外部对象域中的所有数据。这个引用在内部类中是不可见的(类似于外部嵌套的块作用域)
这个步骤是编译器自动在构造器中完成的(不太清楚,可能需要自行进行实验)
从正规来说,外围类引用的语法是比较复杂的。OuterClass.this
表示外围类的引用,其中OuterClass
为外围类的类名。
同时,构造器也可以使用更直观的方式outer Object.new InnerClass(construction parameters)
在外围类的作用域外,可以这样引用内部类:OuterClass.InnerClass
编译器使用了特殊的方法来访问,这原则上是破坏了封装。理论上,熟悉类文件结构的黑客可以创建虚拟机指令来调用指定方法的类文件。
即直接在类方法中定义类,这样它的作用域被限定在声明这个局部块中,完全与外部世界隔绝。
局部类不仅能够访问外部类,还能够访问局部变量。不过,局部变量必须声明为final。
声明为final的原因是需要局部变量与局部类内建立的拷贝要保持一致。
如果仍然需要更新,则该局部变量可以声明为一个长度为1的数组。数组引用不变,但是其值可以改变,有点类似于C++的常数指针。
例子
public void start(int interval,final boolean beep){
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent event){
Date now = new Date();
System.out.pritnln("At the tone, the time is "+now);
if(beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval,listener);
t.start();
}
格式类似于new SuperType(construction parameters){inner class methods and data}
其中SuperType可以是接口或者要扩展的类。由于匿名类没有类名,自然也就没有构造器。取而代之,构造器参数传递给超类的构造器。尤其是内部类实现接口的时候,不能有任何构造参数。
如果使用内部类知识为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。可以将内部类声明为static,以便取消产生的引用。
当然,只有内部类可以声明为static。静态内部类不存在对生成它的外围类的引用,其他完全一样。在静态方法中构造的内部类必须为静态内部类。
运用代理可以在运行时创建一个实现了一组给定接口的新类。该功能只在编译时无法确定需要实现哪个接口时才使用。
我不太感兴趣,直接跳过。
异常分类:所有异常都是由Throwable继承而来。下一层是Error和Exception,Exception下分IOException和RuntimeException。
Error描述了Java运行时系统地内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象,基本无能为力。
如果遇到了无法处理的情况,java方法可以抛出一个异常。因此方法需要告诉编译器可能发生什么错误,在其首部声明可能抛出的异常。public FileInputStream(String name) throws FileNotFoundException
,对构造器进行声明,说明可能会抛出FileNotFoundException
异常。
多个已检查异常应该使用逗号隔开
不需要声明Java的内部错误(从Error继承的错误),因为任何代码都可能抛出,无法控制。同样,也不应该声明从RuntimeException继承的未检查异常,对于这些错误,更应该将时间花费在修正程序中的错误,而不是说明这些错误发生的可能性上。
//1
throw new EOFException();
//或者,2
EOFException e = new EOFException();
throw e;
这个EOFException还接受一个String类型的参数。
可能会遇到标准异常类都没有能够充分描述清楚的问题,需要自定义异常类。需要定义一个派生于Exception的类,或者派生于Exception子类的类。
习惯上,该类要包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类的toString()方法会打印出这些详细信息)
使用
try{
//code
}catch (ExceptionType e){
//do something
}
来完成异常的捕获。如果方法中的任何代码抛出了catch子句中没有声明的异常类型,那么这个方法就会立刻退出。
一个try块可以捕获多个异常,每个异常使用一个单独的catch子句
try{
//some code access db
}catch(SQLException e){
throw new ServeletException("database error:"+e.getMessage());
//或者这样
Throwable se = new ServeletException("database error");
se.initCause(e);
throw se;
//捕获到异常时,可以使用Throwable e = se.getCause();获得原始异常
}
这方面的内容可以看一下这个问题,还挺有意思的。
主要是为了解决资源回收问题,比如关闭说几句
不管是否有异常被捕获,finally子句中的代码都被执行。因为在大部分语言中都有这一部分,我就略过了。
可以调用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述信息。或者使用getStackTrace方法获得StackTraceElement对象的一个数组,从而自行分析。
在python中经常用assert
的选手应该很熟悉这个。即在一些非常关键、非常确信的地方使用该语句,以保证程序的正常运行。
Java中包括assert 条件;
与assert 条件:表达式;
这两种。如果结果为false,则会抛出一个AssertionError异常。第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
感觉和python挺不一样的,更多是作为调试手段。可以在运行程序时使用-enableassertions
或-ea
选项启用。在启用或禁用断言时不必重新编译程序。
也可以在某个包或某个类内使用断言:java -ea:MyClass -ea:com.mycompany.lib... MyApp
断言的使用场景:
日志系统管理着一个Logger.global的默认日志记录器Logger.getGlobal().info("File->Open menu item seelcted");
,会自动包含时间、调用的类名和方法名。
但如果在相应的地方调用Logger.getGlobal().setLevel(Level.OFF);
,将会取消所有的日志。
自定义日志记录器,调用getLogger
方法可以创建或检索记录器
private static final logger myLogger = Logger.getLogger("com.mycompany.myapp")
日志记录器级别:SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST,默认只记录前三个级别。
默认的日志记录将显示日志调用的类名和包名,但如果虚拟机对执行过程进行了优化,就得不到准确地调用信息,此时可以使用logp方法获得调用类和方法的确切位置。
配置文件优先于main方法调用。
不感兴趣,略过。
处理器可以处理日志记录器发来的记录。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为java.util.logging.ConsoleHandler.level=INFO
另外,还可以安装自己的处理器
Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(Level.FINE);
logger.setUserParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);
日志到此略过,感觉这些都好老了。
…建议使用JUnit编写单元测试。
和C++比较类似,我估计一时半会用不上,先跳过。
一些值得注意的点
调用时可以省略泛型,编译器可以根据参数自动推断。
public static <T extends Comparable> T min(T[] a)
Pair与Pair没什么关系。通常,Pair<S>
与Pair<T>
没什么联系。
Pair<? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类。
很容易发现12.4的类型变量限定与其很类似。但它还有一个附加的能力,即可以指定一个超类型限定,如下所示? super Manager
,这个通配符限制为Manager的所有超类型,可以为方法提供参数,但不能使用返回值。
例如void setFirst(? super Manager)
,编译器不知道setFirst方法的确切类型,但是可以用任意Managerr对象调用它,而不能用Employee对象调用。
例如Pair<?>
,返回值一般只能赋予Object
Pair<?>与Pair的不同在于,前者可以用任意Object对象调用原始的Pair类的指定方法
说实话,这部分我建议红书Algorithm,单纯看操作很容易流于形式,还是需要结合具体的算法才比较好。
熟悉STL的话可以直接略过,没什么不同的。
Java类库中的集合类基本接口为Collection接口,有两个基本方法
public interface Collection<E>
{
boolean add(E element);//用于向集合中增加元素
Iterator<E> iterator();//返回一个实现了Iterator接口的对象,可以使用迭代器依次访问集合中的元素
}
迭代器:
public interface Iterator<E>
{
E next();//通过反复调用next,可以逐个访问集合中的每个元素
boolean hasNext();//如果还有多个可访问的元素,返回true
void remove();//将会删除上次调用next方法时返回的元素(即当前所指的元素)。在调用remove前不调用next是不合法的。
}
迭代器的使用,比较优雅的可以使用For-each
for(String element:c){
//do something with element
}
动态数组ArrayList
存在的问题时,从数组中间删除一个元素要付出巨大的代价。
为此实现了链表LinkedList
,可以在任何位置高效地插入和删除。与泛型集合相比,链表是有序集合,其add
方法可以将对象添加到链表的尾部或中间(由迭代器实现)。
链表可能出现问题,详见替代操作
List<String> list = new LinkedList<String>();
ListIterator<String> iter = list.listIterator();
String oldValue = iter.next();//拿到第一个值
iter.set(newValue); //给第一个值设置新值。这与前面remove的逻辑相同,在调用next后才能执行正确的逻辑。
散列表可以很快的计算出散列码,我不太清楚java的hash code是怎么算的,但一般来说都是唯一的。
HashSet
类,散列表集合。该散列表使用的是桶实现,将散列表对桶的总数求余,得到的结果为保存这个元素的桶的索引。会碰上散列冲突问题,因此每个桶内部应该是链表。
树集是有序集合,可以以任意形式插入但是顺序输出。TreeSet
使用的是红黑树,效率会略高于HashSet,但是可以接受。
说实话这个我用的比较多,因为红黑树比较难写。优先队列一般是堆,每次弹出优先级最高的任务。
map映射表,根据某些键的信息来查找与之对应的元素。HashMap是非常常用的工具。
与散列表不同,映射表中键是唯一的,同一个键中后赋的值会直接覆盖先赋的值。
提供了一个从更高角度看类实现的方式,挺有意思的。写起来比较麻烦,建议看原书。
目前为止的大部分例子都使用迭代器来进行,同时也可以使用bulk operation批操作来避免频繁地使用迭代器。
比如
Set<String> result = new HashSet<>(a);
result.retainAll(b);//保留在a与b中都出现的元素看,构成交集
数组变为集合可以使用Arrays.asList
包装器
集合变数组可以使用toArray
方法,但是不能直接调用(因为返回的是Object[]类型),而是应该String[] values=staff.toArray(new String[0])
,将每个值在内部进行类型转换。
后续略过
java并发是非常复杂的,这里只能简单地学习一下
让我来回答的话就是进程内的子程序,是独立调度和分派的基本单元。多线程技术可以把容易阻塞的IO和人机交互功能与密集计算功能分开执行,从而提高程序的执行效率。
如何启动线程
public interface Runnable{
void run();//这个接口非常简单,就只需要一个方法
}
class MyRunnable implements Runnable{
public void run(){
//task code
}
}
Runnable r= new MyRunnable();
Thread t = new Thread(r);
t.start()
PS:如果直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。
自然终止:当线程的run方法执行方法体中的最后一条语句后,经由执行return语句返回,或者出现了方法中没有捕获的异常,线程将终止。
强制终止:调用interrupt方法可以用来请求终止线程。原理是调用该方法后,线程的中断状态将被置位。每个线程都会不时检查这个标识,以判断线程是否被中断。Thread.currentThread().isInterrupted()
但,如果线程被阻塞,就无法检测终端状态。此时会被Interrupted Exception异常中断。
没有任何一个语言方面的需求要求一个被中断的线程应该被终止,中断一个线程不过是为了引起它的注意,被中断的线程可以决定如何响应中断。线程一般将中断你作为一个终止的请求。
如果长期处于阻塞状态,应该检测InterruptedException异常
public void run(){
try{
while(more work to do){
//do more work
Trhead.sleep(delay);
}
}catch(InterruptedException e){
//thread was interrupted during sleep
}finally{
//clean up, if required
}
//exiting the run method terminates the thread
}
线程有六种状态:
要确定一个线程的当前状态,可以调用getState
方法
New:当调用new Thread(r)
时,线程还没有开始运行
一旦调用thread的start方法,就是可运行状态。其是否运行,取决于操作系统给线程提供运行的时间。且一旦一个线程开始运行,它不必始终保持运行
此时暂时不活动,直到线程调度器重新激活它。
包括线程优先级、守护线程、线程组以及处理未捕获异常的处理器。
线程优先级:默认情况下,一个线程继承它的父线程的优先级,可以使用setPriority
方法设置一个MIN_PRIORITY
1与MAX_PRIORITY
10之间的任何值,默认是5.
可以通过t.setDaemon(true)
将线程转换为守护线程,为其他线程提供服务。如果只剩下守护线程,VM将退出。
守护线程不应该去访问固有资源,如文件、数据库因为它会在任何时候甚至任何一个操作的中间发生中断。
线程的run方法不能抛出任何被检测的异常。但是也不需要catch子句来处理可被传播的异常,在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须属于一个实现Thread.UncaughtExceptionHandler
接口的类,这个接口只有一个方法void uncaughtException(Thread t,Throwable e)
。可以使用setUncaughtExceptionHandler
方法为任何线程安装一个处理器,也可以用静态方法setDefaultUncaughtExceptionHandler
为所有线程安装一个默认的处理器。
这个就是操作系统的相关知识了,不赘述
有两种机制防止代码块受到并发访问的干扰,一个是synchronized
关键字,它自动提供了一个锁以及相关的条件,在需要显式锁的时候是很便利的。
第二个是ReentrantLock
类,使用例子如下:
myLock.lock(); // a ReentrantLock object
try{
//critical section
}finally{
myLock.unlock();
}
这一结构确保任何时候只有一个线程进入临界区。一旦一个线程封锁了对象,其他任何线程都无法通过lock
语句,他们会被阻塞直到第一个线程释放锁对象。
PS:把解锁语句放在finally中至关重要,不然临界区的代码如果抛出异常,锁必须释放。
线程进入临界区之后,要满足某一条件才能执行。该条件限制获得锁但是不能做有用工作的线程。
可以使用conditionName = newCondition()
方法来设定新的条件对象。如果不满足条件,线程会调用conditionName.await()
方法放弃锁。放弃锁与没获得锁有本质上的不同,它的阻塞状态直到条件满足后才能解除。
条件满足使用conditionName.signalAll()
满足,激活所有因为这一条件等待的线程。
如果一个方法使用synchronized关键字声明,那么对象的锁将保护整个方法。即下面两个是相等的
public synchronized void method(){
//method body
}
//等价于
public void method(){
this.intrinsicLock.lock();
try{
//method body
}finally{
this.intrinsicLock.unlock();
}
}
每一个Java对象有一个锁,线程可以通过调用同步方式获得锁。另外一个获得锁的机制就是进入一个同步阻塞
synchronized(obj){
//critical section
}
监视器monitor,可以在不需要程序员考虑加锁的情况下,保证多线程的安全性。
监视器的特性:
obj.method()
,则obj对象的锁在方法调用开始时自动获得,并且当方法返回时自动释放。但是Java类和监视器差距很大
如果仅仅为了读写一个或两个实例域而使用同步,开销过大。
volatile域为实例域的同步访问提供了一种免锁的机制。如果声明一个域为volatile
,那么编译器和虚拟机就知道该域可能被另一个线程并发更新。
private boolean done;
public synchronized boolean isDone(){return true;}
public synchronized void setDone(){done = true;}
//上述使用内部锁,但是并不一定是个好主意
private volatile boolean done;
public boolean isDone(){return true;}
public void setDone(){done = true;}
//但是不保证原子性
java.util.concurrent.locks包定义了两个锁类,即ReentrantLock
类和ReentrantReadWriteLock
类。后者解决了操作系统中的读/写者问题,允许对读者线程共享访问控制,写者进行互斥访问控制。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
与private Lock writeLock = rwl.writeLock();
public double getTotalBalance(){
readLock.lock();
try{
//some method
}finally{
readLock.unlock();
}
}
public void transfer(...){
writeLock.lock();
try{
//some method
}finally{
writeLock.unlock();
}
}
应该是为了解决生产者-消费者问题。实际编程应该尽量原理基本结构,并使用高层结构。
java.util.concurrent包提供了阻塞队列的几个变种。
如果多线程要并发地修改一个数据结构,那会很容易破坏它。
java.util.concurrent包提供了映射表、有序集和队列的高效实现:
ConcurrentHashMap
ConcurrentSkipListMap
ConcurrentSkipListSet
ConcurrentLinkedQueue
与大多数集合不同,size方法不必在常量时间内操作,而是需要遍历。
Runnable
封装一个异步运行的任务。Callable
与其类似,但是有返回值。
public interface Callable<V>{
V call() throws Exception;
}
Future
保存异步计算的结果(有点类似最近看到最新JS的那个future)。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future的所有者在结果计算好之后就可以获得它。
public interface Future<V>{
V get() throws ...;
V get(long timeout, TimeUnit unit) throws;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDone();
}
第一个get方法的调用被阻塞,直到计算完成;如果在计算完成之前,第二个方法的调用抄书,抛出一个TimeoutException异常。如果运行该计算的线程被中断,两个方法都将抛出InterruptedException。如果计算已经完成,那么get方法将立即返回。
可以使用cancel
方法取消计算。
FutureTask包装器,可以将Callable转换成Future和Runnable,它同时实现两者的接口:
Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t = new Thread(task);//task is a Runnable
t.start();
...
Integer result = task.get();//task is a Future
构建线程有一定的代价。如果程序中创建了大量生命期很短的线程,应该使用线程池(thread pool)。线程池中包含许多准备运行的空线程,将Runnable对象交给线程池,就会有一个线程调用run方法。
当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由:减少并发线程的数目。
执行器Executor类,使用许多静态工厂方法来构建线程池:
newCachedThreadPool
。构建线程池,如果有空闲线程可用则立刻执行;否则,创建一个新线程。newFixedThreadPool
newSingleThreadExecutor
newScheduledThreadPool
newSingleThreadScheduledExecutor
java.util.concurrent包提供能帮助人们管理相互合作的线程集的类: