十三、字符串
1 知识
String对象是不可变的。String对象方法等之间的传递实际上是引用的一个拷贝。而该引用所指向的对象一直待在单一的物理位置上,从未动过。
String对象具有只读特性,所以指向它的任何引用都不可能改变它的值,也不会对其他的引用有什么影响。
在使用字符串连接时如果拿不准用哪种方式,可以用javap来分析程序代码。查看java代码是如何工作的可以用javap反编译代码:
javap -c Demo //(类名) -c表示将生成JVM字节码
重载“+” 与StringBuilder
通过javap反编译可看到,在使用重载“+”时,编译器或自动帮我们创建一个StringBuilder对象来构造连接出最终的String对象。但是要注意的时,在循环体里如果还是用重载“+”来连接String对象的话,编译器自动创建的StringBuilder是在循环体内产生的,这意味着每一次循环都会创建一个新的StringBuilder对象。因此,在连接循环体内的String对象时,要自行创建StringBuilder对象在循环体外append()拼接,并且在使用append()方法连接时禁用append(a+ ":" +c)这种投机取巧的方式,否则编译器又会自动创建一个StringBuilder对象来连接内部字符串操作;如果是简单的字符串连接(特别是不是循环体的连接的话),可以信任编译器来处理。
StringBuilder是线程不安全的,但也因此效率比StringBuffer快一点。
@Test
public void stringTest() {
StringBuffer sb = new StringBuffer();
sb.append("a").append("+").append("b").append("=").append("c").append(" ok");
//delete(int start, int end) 可以用于删除不想要的字符串连接。
sb.delete(sb.length()-2,sb.length());
System.out.println(sb);
}
无意识递归
public class InfiniteRecursion {
@Override
public String toString() {
//String"+"会自动类型转换为Sting,会想把InfiniteRecursion转化成String,这时候调用toString方法,又会到了原点,造成无意识递归,所以禁用this
//return "InfiniteRecursion{}"+this;
//正确的做法是用super.toString()
return "InfiniteRecursion{}"+super.toString();
}
public static void main(String[] args) {
List<InfiniteRecursion> list = new ArrayList<>();
for (int i =0;i<5;i++){
list.add(new InfiniteRecursion());
}
System.out.println(list);
}
}
String的基本方法:P321
@Test
public void stringTest1() {
String 啊 = "我是一个大头兵";
char c = 啊.charAt(6);
System.out.println(c);
String a = "i am iron man";
char c1 = a.charAt(6);
System.out.println(c1);
//char是字符形式
char[] b = new char[50];
a.getChars(0,a.length(),b,0);
System.out.println(b);
//byte 是字节形式
byte[] bytes = a.getBytes();
System.out.println(Arrays.toString(bytes));
//生成字符串的所以字符数组
char[] chars = a.toCharArray();
System.out.println(chars);
boolean we = a.contentEquals("i am iron man");
boolean b1 = a.contentEquals(new StringBuffer("i am iron man"));
System.out.println(we +" "+ b1);
//比较string是否相等
boolean regionMatches = a.regionMatches(3, "am iron man", 0, "am iron man".length());
System.out.println(regionMatches);
System.out.format("test%d",5);
}
Java intern() 方法
Formatter 是个解释器
正则表达式
一般来说,正则表达式就是以某种方式来描述字符串。
java语言对反斜线\的处理与其它语言不同。
如: \d = java : \ \d .
\ \ \ \ :java用此表示一条普通的反斜线。
斜线是:/ : 左正右反 :\
正则表达式中有特殊意义的字符都要通过\ \ 双反斜线来转义。如+ :\ \ +
如果正则表达式不是只使用一次的话,非String对象的正则表达式具备更加的性能。如用于匹配手机号格式等的工具类。
匹配规则:
CharSequence
接口CharSequence是从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。所以这些实现了CharSequence的类都可以用于接收CharSequence参数的方法的使用;多数正则表达式操作也都接收CharSequence类型的参数。
Pattern和Matcher
使用Pattern pattern = Pattern.compile("^[A-Z].*。$");编译正则表达式。
组(Groups)
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号为1表示被第一对括号括起来的组,以此类推。
//三个组 0:ABCD 1: BC 2:C
A(B(C))D
组的概念用得少,先不仔细了解。
例子:
@Test
public void matchTest() {
//通过String的方法如matches是应用正则表达式最简单的途径
boolean b = "-1234".matches("-?\\d+");
System.out.println(b);
//330
//在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。 定义为私有静态类常量
// private static Pattern NUMBER_PATTERN = Pattern.compile("[0-9]+");
// 说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则)
//首字母大写,句号结束。注意.*都是特殊有特殊含义的,不需要转义,转义了就变成普通字符了
Pattern pattern = Pattern.compile("^[A-Z].*。$");
boolean matches = pattern.matcher("W水电费个胜多负。").matches();
System.out.println(matches);
pattern = Pattern.compile("W");
//如果是用于replace、split方法则不要加(^$)
String s = pattern.matcher("SSSSW水电费个胜多负。").replaceAll("WWW");
System.out.println(s);
String[] split = pattern.split("SSSSW水电W费个wW胜多负W。");
System.out.println(Arrays.toString(split));
//限制分割字符串数量最多3个
String[] split1 = pattern.split("SSSSW水电W费个wW胜多负W。", 3);
System.out.println(Arrays.toString(split1));
pattern = Pattern.compile(",");
String target = "i want to say , you either die hero, or live long enough to see youself become a villain,";
StringBuffer sb = new StringBuffer();
Matcher matcher = pattern.matcher(target);
while (matcher.find()) {
//) 将当前匹配子串替换为指定字符串,并且将替换后的子串以及其之前到上次匹配子串之后的字符串段添加到一个StringBuffer对象里
//appendReplacement可以对匹配到的数据做不同类型的处理,通过方法提供不同的replacement即可。
matcher.appendReplacement(sb, returnRandom());
System.out.println(sb);
}
//将最后一次匹配工作后剩余的字符串添加到一个StringBuffer对象里。
System.out.println(matcher.appendTail(sb));
//将matcher对象重新设置到当前字符序列的起始位置
matcher.reset();
System.out.println( matcher.replaceAll(""));
//应用新的字符序列
matcher.reset("happy ending , ok?");
System.out.println( matcher.replaceAll(""));
}
private static Random random = new Random(47);
private String returnRandom() {
int i = random.nextInt(20);
return String.valueOf(i);
}
正则表达式可用于如日志搜索,如linux系统中的grep 命令。
如下,通过对每一行reset匹配对象,然后如果find为true,则通过group打印出值和start打印出位置。
Scanner
Scanner类大大减轻了扫描输入的工作负担。Scanner构造器可以接受任何类型的输入对象,包括File、String、InputStream、Readable等对象。所有的输入、分词以及翻译操作都隐藏在不同类型的next方法中。
例子:
/**
* 防火墙日志文件扫描
*/
private static String log =
"10.25.253.123@31/12/2018\n"+
"11.25.253.123@31/11/2018\n"+
"12.25.253.123@31/10/2018\n"+
"13.25.253.123@31/09/2018\n";
@Test
public void fireScannerTest(){
Scanner scanner = new Scanner(log);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)){
//scanner.next(pattern)如果不需要可以不返回值,但一定要调用,如果不调用scanner.next(pattern),那么匹配指针位置就会一直不动保持在第一位,在while循环模式下一直循序下去不结束
String next = scanner.next(pattern);
//返回匹配值
System.out.println(next);
MatchResult match = scanner.match();
//match.group(0)指整个匹配值,1是第一个括号的值;2是第二个括号的
System.out.println("group0="+match.group(0));
System.out.println("ip="+match.group(1)+"===date="+match.group(2));
}
}
2 疑问
javap反编译出来的程序代码是汇编语言么?怎么看?
不是,是java自己的定义的八种原子操作,这个要参考操作文档查看具体的意思就懂了。
String.contentEquals(CharSequence cs)方法中的CharSequence 是什么类型?
A:接口CharSequence是从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。所以这些实现了CharSequence的类都可以用于接收CharSequence参数的方法的使用;多数正则表达式操作也都接收CharSequence类型的参数。
收集正则表达式中的一些生活常用语句,如邮箱、手机号等匹配规则。
定界符?
A:就是 设定界限的 符号。 比如字符 a,就需要用单引号做定界符 'a'; 比如字符串 abc,就需要用双引号做定界符 "abc"。 就是 告诉计算机: 字符开始了a字符结束了。 字符串开始了abc字符串结束了。
在正则表达式中定界符就是(^.***$)
3 思想总结
总的来说一般字符串的操作就是一些普通常用的操作,只要熟练就行,如果需要字符串拼接的话看情况选用拼接方法。正则表达式的匹配一般是用在像手机号,电子邮箱这种的验证上,比较简单;而如果是用在日志搜索匹配解析的话就比较复杂,针对解析日志等文件需要Scanner来处理更方便灵活。
十四、类型信息
1 知识
RTTI——Run-Time Type Identification:在运行时识别一个对象的类型。
多态:同一个行为具有多个不同表现形式或形态的能力。
运行时类型信息使得你可以在程序运行时发现和使用类型信息。将我们从智能在编译期执行面向类型的操作禁锢中解脱出来。
运行时识别对象和类的信息方式有二:
- RTTI.RTTI假设我们在编译时已经知道了所以的类型。
- 反射机制。反射允许我们在运行时发现和使用类的信息。
面向对象编程的基本目的:让代码只操作对基类的引用。这样如果要添加新类扩展程序变不会影响原代码(多态)。我们希望大部分代码都用多态调用,尽可能少地了解对象的具体类型,使得代码更容易读、写和维护;设计更容易实现、理解和改变。(多态时,虽然我们在写代码时只让这个类或对象表现出其父类的类型,但是在运行时这个对象一直是他该有的具体类型,只不过是他不停的换着面具表示他是这一类,那一类,甚至是Object(God))
我们对抽象的基类用抽象类或者接口表示可以防止对实例化无意义的基类实例化。
Class对象
要理解RTTI在java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作由特殊对象——Class对象来完成,它包含了与类有关的信息。
Class对象就是用来创建类的所以的“常规”(程序要运行使用的)对象的。java使用Class对象来执行其RTTI,像转型这样的操作也是。每个类就是一个Class对象,经过虚拟机编译后就是个 类名.class文件,通过类加载器加载。
所以类都是在对其第一次使用时才动态加载到JVM中的。第一次使用指程序创建第一个对类的静态成员的引用时(构造器也是类的静态方法,因此在使用new创建类的新对象时也是对类的静态成员的引用),就会加载这个类。
如果尚未加载,默认的类加载器就会根据类名查找.class文件(也可能会在数据库中查找字节码)。
使用newInstance()创建的类,必须带有默认的构造器。
//不带默认构造器会报实例化异常
java.lang.InstantiationException: com.fcar.thinkjava.duotai.Sandwich
at java.lang.Class.newInstance(Class.java:364)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:385)
生成Class对象的引用方式:
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
类初始化步骤:
- 加载。通过类加载器查找字节码创建Class对象。
- 链接。验证字节码,为静态域分配存储空间,如有需要将类的符合引用替换为直接引用。
- 初始化。如果有父类,先对父类初始化,执行静态初始化器(包括构造器)和静态初始化块(static 修饰的代码块或域)
Java初始化的原则是尽可能的“惰性”。
仅使用.class语法来获得对类的引用不会引发初始化。
编译器常量的读取不需要对类进行初始化。
如果是一个被static final 修饰的值(编译器常量),那么用类调用读取值不需要对类进行初始化(因为在解析阶段就会把常量的值直接赋值给常量引用)(该类的static代码块什么的不会执行,因为没有初始化)。但是如果被static final 修饰的值 不是编译器常量(如值是通过random调用获得的,那么读取该值还是会执行初始化)。如果只是static修饰(没有final),则肯定要执行初始化。
泛化的Class引用
Class引用总是指向某个Class对象,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。
通过使用泛型语法,可以让编译器强制执行额外的类型检查。(否则普通的类引用是可以被重新赋值的)
通配符 “?” 表示“任何事物”。
//表示获取的是Cycle的父类的类引用
Class<? Super Cycle> = Cycle.getSuperClass();
//表示获取的是Cycle的子类的类引用
Class<? extend Cycle> = someClass;
//转型方式有2(也叫向下转型):
Class<Cycle> cycleT = Cycle.class;
//1
Cycle c = cycleT.cast(a);
//2 直接强转,一般都这样
(Cycle)a;
RTTI形式包括:
- 传统的类型转换。如(Shape)cycle。
- 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
- 关键字 instanceof 。用于判断是否特定类型的实例。
instanceof 只可与命名类型进行比较,而不能与Class对象作比较。注意:如果程序中编写了许多的instanceof 表达式,就说明设计存在瑕疵。
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Object o1 = name.newInstance();
//name.isInstance等同于instanceof
System.out.println("isInstance="+name.isInstance(o1));
构造器就算一个工厂方法模式。
P364 工厂方法实例待实践。
instanceof 与Class的等价性
instanceof 与isInstance()保持了类型的概念,指的是你是这个类或者这个类的子类么?
而用==或equal()比较时不考虑继承,比较的是确切的类型是否相同。
反射:运行时类信息
Class类与java.lang.reflect(Field,Method,Constructor)类库一起对反射的概念进行了支持。
通过反射,我们可以使用Constructor创建新的对象,用get()和set()方法(调用Method的方法)读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。还可以调用Class.getMethods(),getFields() ,getConstructors()返回表示方法、字段以及构造器的对象的数组。
RTTI与反射的真正区别只在于:对于RTTI,编译器在编译时打开和检查.class文件;而对于反射机制,编译器在运行时打开和检查.class文件(因为编译时不可获取);
反射在java中是用来支持其他特性的,例如对象序列化和JavaBean。
运行时才获取类信息的动机:
- 需要获取一个指向某个并不在你的程序空间中的对象的引用(如磁盘文件、网络连接中)。
- 希望提供在跨网络的远程平台上创建和运行对象的能力(远程方法调用RMI)。
反射测试类:
@Test
public void reflectTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的构造方法==============================");
Constructor<?>[] constructors = clazz.getConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println( constructors[i]);
}
System.out.println("===========================获取所有的构造方法==============================");
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (int i = 0; i < declaredConstructors.length; i++) {
System.out.println( declaredConstructors[i]);
}
System.out.println("=============================获取公有 & 有参的构造方法============================");
Constructor<?> constructor = clazz.getConstructor(String.class);
System.out.println(constructor);
System.out.println("=============================获取私有 & 有参 构造方法============================");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Integer.class);
System.out.println(declaredConstructor);
System.out.println("=============================获取公有 & 无参的构造方法============================");
Constructor<?> constructor1 = clazz.getConstructor(null);
System.out.println(constructor1);
System.out.println("=============================构建有参构造器对象============================");
Object o = constructor.newInstance("constructorTest");
System.out.println(o);
}
@Test
public void reflectFieldTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的域==============================");
Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
System.out.println( fields[i]);
}
System.out.println("===========================获取所有的域==============================");
Field[] declaredFields = clazz.getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
System.out.println( declaredFields[i]);
}
System.out.println("=====获取公有字段并使用=====");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//获取字段名称
//直接get
Object publicField1 = publicField.get(instance);
System.out.println("publicField.get+++"+publicField1);
//通过多态获取
System.out.println( reflectTest.getPublicField());
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通过多态+反射获取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
System.out.println("=====获取私有字段并使用=====");
Field privateField = clazz.getDeclaredField("privateField");
//设置私有域要暴利反射
//在获取私有属性的时候如果没有进行暴力反射,那么会抛出异常。
//java.lang.IllegalAccessException: Class com.fcar.guava.GuavaTest can not access a member of class com.fcar.thinkjava.type.ReflectTest with modifiers "private"
privateField.setAccessible(true);
System.out.println("====before====="+reflectTest.getPrivateField());
privateField.set(instance,"privateTest");
System.out.println( reflectTest.getPrivateField());
}
@Test
public void reflectMethodTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的方法==============================");
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println( methods[i]);
}
//否则会走下去
Thread.sleep(2000);
System.out.println("===========================获取所有的方法==============================");
Method[] declaredMethods = clazz.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
System.out.println( declaredMethods[i]);
}
Thread.sleep(5000);
System.err.println("======获取特定方法(带参)并使用=====");
Object instance = clazz.getConstructor().newInstance();
Method commonActionWithParam = clazz.getMethod("commonActionWithParam", String.class);
System.out.println(commonActionWithParam);
commonActionWithParam.invoke(instance,"METHODPARAM");
Thread.sleep(2000);
System.err.println("======获取特定方法(不带参)并使用=====");
Method commonAction = clazz.getMethod("commonAction");
System.out.println(commonAction);
commonAction.invoke(instance);
Thread.sleep(2000);
System.err.println("======获取私有方法(不带参)并使用=====");
//Method privateAction = clazz.getMethod("privateAction");
Method privateAction = clazz.getDeclaredMethod("privateAction");
//调用私有方法一定要设置权限
privateAction.setAccessible(true);
System.out.println(privateAction);
privateAction.invoke(instance);
}
@Test
public void reflectStaticTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================反射执行STATIC方法==============================");
Method staticAction = clazz.getMethod("staticAction");
staticAction.invoke(null);
}
@Test
public void reflectMainTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.guava.GuavaTest");
System.out.println("===========================反射执行main==============================");
Method main = clazz.getMethod("main", String[].class);
//Object o = clazz.getConstructor().newInstance();
//main 是static方法不需要创建对象
main.invoke(null,(Object) new String[]{"A"});
}
public static void main(String[] args) {
System.out.println("I AM MAIN");
}
动态代理
使用场景:
你希望跟踪目标对象中的方法的调用,或者希望度量这些调用的开销,这些代码并不希望将它们合并到应用的代码中,因此代理使得你可以很容易的添加或移除它们。
在Spring项目中用的注解,例如依赖注入的@Bean、@Autowired,事务注解@Transactional等都有用到,换言之就是Spring的AOP(面向切面编程时)。
举个例子:为什么@Transactional 注解只有public权限的才能生效呢?
A:因为@Transactional注解使用时是通体Spring的AOP(面向切面编程)实现的,这时候就用到了动态代理,而动态代理时因为生成的动态代理类实际上是个实现了被代理类的接口的类,而接口中的方法默认都是public的,所以是不可能有其他访问权限的方法生效的,因为动态代理代理不到这个方法。
动态代理的好处是比较灵活,可以在运行的时候才切入改变类的方法,而不需要预先定义它。
JDK动态代理:
@Test
public void dynamicProxyTest() {
TeaMan teaMan = new WuYiShanTeaMan();
InvocationHandler guangZhou13Hang = new GuangZhou13Hang(teaMan);
TeaMan guangZhou13HangProxy =(TeaMan) Proxy.newProxyInstance(teaMan.getClass().getClassLoader(), teaMan.getClass().getInterfaces(), guangZhou13Hang);
guangZhou13HangProxy.sellTea(5000);
guangZhou13HangProxy.buyOpium(3000);
}
public class WuYiShanTeaMan implements TeaMan {
@Override
public void sellTea(Integer hearvy) {
System.out.println("WuYiShanTeaMan sellTea "+hearvy+" KG");
}
@Override
public void buyOpium(Integer hearvy) {
System.out.println("WuYiShanTeaMan buyOpium "+hearvy+" KG, what a shame");
}
}
//接口
public interface TeaMan {
public void sellTea(Integer hearvy);
public void buyOpium(Integer hearvy);
}
//实现InvocationHandler
public class GuangZhou13Hang implements InvocationHandler {
private TeaMan teaMan;
public GuangZhou13Hang(TeaMan teaMan) {
this.teaMan = teaMan;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===================before====================");
Object invoke;
if (method.getName().equals("buyOpium")) {
System.out.println("官府收账款放行鸦片");
invoke = method.invoke(teaMan, args);
System.out.println("官老爷逛窑子");
} else {
invoke = method.invoke(teaMan, args);
}
System.out.println("===================after====================");
return invoke;
}
}
cglib动态代理:JDK的动态代理一定要继承一个接口,如果要基于POJO类的动态代理 ,那么可以用cglib。
@Test
public void cglibDynamicProxyTest() {
System.setProperty( DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"F:\\mywork\\fc-starter");
AmericanBusinessMan businessMan = new AmericanBusinessMan();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(EastIndiaCompany.class);
enhancer.setCallback(businessMan);
EastIndiaCompany o = (EastIndiaCompany)enhancer.create();
o.buyTea(10000);
o.plantOpium(30000);
}
//代理要实现MethodInterceptor
public class AmericanBusinessMan implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("===================before Opium====================");
Object invoke;
if (method.getName().equals("plantOpium")) {
System.out.println("India MI FANS IN DANGER");
invoke = methodProxy.invokeSuper(o,objects);
System.out.println("india MI FANS GOODBYE");
} else {
invoke = methodProxy.invokeSuper(o, objects);
}
System.out.println("===================after Opium====================");
return invoke;
}
}
//被代理类
public class EastIndiaCompany {
public void buyTea(Integer hearvy) {
System.out.println("EastIndiaCompany buyTea "+hearvy+" KG");
}
public void plantOpium(Integer hearvy) {
System.out.println("EastIndiaCompany plantOpium "+hearvy+" AREA");
}
}
空对象
引入空对象后,我们可以假设所有的对象都是有效的,而不必浪费编程精力去检查null。
2 疑问
以下的报错实际上是因为我们用Class.forName()后并没有创建对象,这时候报错是因为对象还没有创建,getClass()要用实例去调用才能保证该class已经加装到虚拟机中了。
//成功
GuavaTest test = new GuavaTest();
Class instance1 = test.getClass();
Object o = instance1.newInstance();
//报错
Class<?> name2 = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Class instance2 = name.getClass();
Object o2 = instance2.newInstance();
java.lang.IllegalAccessException: Can not call newInstance() on the Class for java.lang.Class
at java.lang.Class.newInstance(Class.java:344)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:390)
为什么1.7版本通过 Class.forName获取类的引用不会立即进行初始化了呢(编程思想里的会。)?
Class<?> initable3 = Class.forName("com.fcar.thinkjava.type.Initable");
在什么情况下我们喜欢用反射?(毕竟自己拼接方法来调用乏味费时)
动态获取对象或者创建对象时。
有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//获取字段名称
//System.out.println( publicField.getName());
System.out.println( reflectTest.getPublicField());
A:如果我们代码里没有这个类,那么我们可以约定一个基类接口,定义get/set域方法,让我们从远处,比如说数据库获取的类实现这个基类,这样我们就可以在获取域值时,只强转为基类接口类型,然后通过多态加反射来拼接get方法来获得域的值。
public class ReflectTest implements GetSet{
@Override
public Object getField(Class clazz, Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException {
Method commonAction = null;
try {
commonAction = clazz.getMethod("get"+(publicField.getName().substring(0, 1).toUpperCase() + publicField.getName().substring(1)));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return commonAction.invoke(instance);
}
public interface GetSet {
public void setField(Field publicField, Object val);
public Object getField(Class clazz,Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException;
}
//調用
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通过多态+反射获取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
多态与RTTI的区别和联系?面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。这句话怎么理解?
A:我的理解是多态其实就是通过RTTI来实现的,只是多态是基于父类类型来调用子类的具体方法,可是多态是不能调用子类的扩展方法的,因为发现不到;但如果通过RTTI判断出其子类类型,便能强转为子类类型,也就可以调用子类相对于父类的扩展方法了。
现实代码中很少使用空对象?最多便是google的通过Optional判空,空对象用处不大?
null容易导致空指针,这要求被调用方要写非常多的判空代码,如果没有判空容易造成NPE.
3 思想总结
所有的类型修饰符修饰的方法和域、构造器都可以被反射访问到。但是final域在遭遇修改时是安全的。它不会发生任何修改。
RTTI允许通过匿名基类的引用来发现类型信息。
面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。
如果使用多态时基类未包含我们想要的方法,那么我们可以使用RTTI,继承一个新类,添加自己需要的方法。
反射这一块与动态代理在项目框架中用得比较多,需要熟悉使用。
十五、泛型
1 知识
理解了边界所在,你才能成为程序高手。因为只有知道了某个技术不能做到什么,你才能更好地做到所能做的。
Java泛型的优点与局限:
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类(可以是基类或接口)。如果要编写可以应用于多种类型的代码,可以使用泛型。
一个类,如果我们在需要说明类型的地方都使用基类或接口,这样确实能够具备更好的灵活性,但也会有一些性能损耗;并且即使使用了接口,对程序的约束还是太强,因为这要求写的代码必须使用特定的接口。而我们通过使用泛型,可以是代码能够应用于“某种不具体的类型(在用该类时传进来类型<T>)。
泛型实现了参数化类型的概念。使代码可以应用于多种类型。
使用场景:用于创建容器类。
JAVA泛型的核心概念:告诉编译器想要使用什么类型,然后编译器帮你处理一切细节。
@Test
public void typeTest() {
Holder<String> holder = new Holder<>("I","LOVE","U");
System.out.println(holder);
holder.setFirst("she");
holder.setSecond("is");
holder.setThird("mine");
System.out.println(holder);
Holder<Integer> intHolder = new Holder<>(1,2,3);
System.out.println(intHolder);
}
//泛型类
public class Holder<T> {
private T first;
private T second;
private T third;
public Holder(T first, T second, T third) {
this.first = first;
this.second = second;
this.third = third;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
public T getThird() {
return third;
}
public void setThird(T third) {
this.third = third;
}
@Override
public String toString() {
return "Holder{" +
"first=" + first +
", second=" + second +
", third=" + third +
'}';
}
}
元组类库
元组:它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(也称数据传送对象、信使。不允许修改是因为属性值被final修饰)。
利用继承机制可以实现长度更长的元组。
@Test
public void tupleTest() {
Map<String,String> map= new HashMap<String,String>();
map.put("where","home");
ThreeTuple<String,Integer,Map<String,String>> threeTuple = new ThreeTuple<>("baobao",40,map);
System.out.println(threeTuple.toString());
System.out.println(threeTuple.third);
//threeTuple.first = "xiaobao";
}
//二维元组
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A first, B second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "TwoTuple{" +
"first=" + first +
", second=" + second +
'}';
}
}
//三维元组继承自二维
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A first, B second, C third) {
super(first, second);
this.third = third;
}
@Override
public String toString() {
return "ThreeTuple{" +
"third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
泛型接口
泛型接口就是对接口进行泛型,等实现类实现接口时传入想要的类型即可。
public interface Generator<T> {
T getNext();
}
public class CoffeeGenerator implements Generator<Coffee> {
}
泛型方法
我们可以在类中包含参数化方法(参数化方法的意思就是使用泛型),而这个方法所在的类可以是泛型类,也可以不是。是否拥有泛型方法,与其所在的类是否泛型没有关系。
泛型方法使得该方法能够独立于类而产生变化。
基本指导规则:无论何时,只有你能做到,就应该尽量使用泛型方法。如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为更清楚明白。
使用泛型方法不要指明参数类型,因为编译器会进行类型参数推断。(像可以无数次重载的方法)。如果传入的是基本类型,那么自动包装机制会介入自动包装。
可变参数与泛型方法
泛型方法与可变参数列表能够很好地并存。
参考 Arrays.asList(); 方法
@Test
public void TypeTest1() {
List<String> a = makeList("A");
System.out.println(a);
//Serializable[3]@750 类型不同则泛型会找出他们通用的接口或父类作为泛型容器类型
System.out.println(makeList(1,2,"3"));
System.out.println(makeList("A","B","C"));
System.out.println(makeList("I LOVE MORTY,AND I HOPE MORTY LOVE ME".split("")));
}
public static<T> List<T> makeList(T ... args){
List<T> list = new ArrayList<>();
for (T t : args) {
list.add(t);
}
return list;
}
泛型还可以应用于内部类以及匿名内部类。
构建复杂模型
泛型的一个重要好处是能够简单而安全地创建复杂的模型。
泛型擦除
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
Java泛型是使用擦除来实现的。这意味着在使用泛型时,具体的类型信息都被擦除了,List<String> 和List<Integer> 都被擦除成原生类型List。
当我们希望代码能够跨多个类工作时,使用泛型才有所帮助。
@Test
public void TypeTest2() {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.simulator();
}
//普通对象
public class HasF {
public void f(){
System.out.println("HasF f()");
}
}
//泛型类。边界<T extends HasF> 声明T 必须具有类型HasF或者是其子类,这时候我们便能使用obj.f();方法了。
//
public class Manipulator<T extends HasF> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void simulator(){
obj.f();
}
}
java泛型的缺点在于用擦除实现泛型:
擦除减少了泛型的泛化性,如果不使用擦除来实现,而是使用具体化,使类型参数保持为第一类实体,那么他将能够在类型参数上执行基于类型的语言操作和反射操作。
泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。如List<T>将被擦除为List(即容器类型的被擦除为List<Objcet>), 普通的类型变量在未指定边界的情况下被擦除为Object.
擦除的核心动机是为了“迁移的兼容性”,使得泛化的客户端可以用非泛化的类库来使用。
用擦除来实现java泛型的优缺点:
优点是兼容非泛化代码,使我们可以从容的把非泛化代码转变为泛化代码。
缺点是擦除实现的泛型参数类型信息丢失,不能用于显示地引用运行时类型的操作之中,如转型、instanceof操作和new表达式。
擦除和迁移兼容性意味着使用泛型并不是强制的。
P412对于泛型类型的反编译可以看到,两种写法是一样的。
泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。
擦除的补偿
new T()无法实现的原因:一是因为泛型的类型擦除;二是因为编译器不能验证T具有默认(无参)构造器。
java解决无法创建泛型实例( 无法 new T() )的方案是传递一个工厂对象,使用它来创建新的实例。最便利的工厂对象就是Class对象,使用类型标签(构造器使用 Class<T> clazz),就可以使用clazz.newInstance()创建新对象。这种要求创建的对象必须有默认的构造器,否则会运行时异常。要避开这种异常可以选用显式的工厂,限制其类型,使得只能接受实现了这个工厂的类,这样就不会有缺少默认构造器问题。
P414 工厂的选用。
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
//因为我们不能声明
T[] array = new T[sz];
//只能强转
T[] array = (T[])new Object[sz];
因为有了擦除,数组的运行时类型就只能是Object[].如果我们立即将其转型为T[],那么在编译器该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最后是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。
边界
设置边界的目的是为了可以按照自己的边界类型来调用方法,而不是只能调用Objcet的方法。
java使用extends关键字来限制泛型的边界。
实例:
public class EpicBattle {
static <POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoises();
}
static <power extends SuperHearing & SuperSmell> void useFind(SuperHero<power> hero){
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
System.out.println("===================");
useFind(dogBoy);
List<? extends SuperHearing> dogBoyes1;
//<? extends SuperHearing & SuperSmell> 之所以不能编译通过是因为&这个是用于定义泛型时写的,使用泛型时不能这样写。
//List<? extends SuperHearing & SuperSmell> dogBoyes2;
}
}
interface SuperPower{
}
interface XRayVision extends SuperPower{
void seeThroughWalls();
}
interface SuperHearing extends SuperPower{
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower{
void trackBySmell();
}
class SuperHero< POWER extends SuperPower>{
POWER power;
public SuperHero(POWER power) {
this.power = power;
}
POWER getPower(){
return power;
}
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>{
public SuperSleuth(POWER power) {
super(power);
}
void see(){
power.seeThroughWalls();
}
}
class CanineHero<power extends SuperHearing & SuperSmell> extends SuperHero<power>{
public CanineHero(power power) {
super(power);
}
void hear(){
power.hearSubtleNoises();
}
void smell(){
power.trackBySmell();
}
}
class SuperHearSmell implements SuperSmell,SuperHearing{
@Override
public void hearSubtleNoises() {
System.out.println("hearSubtleNoises");
}
@Override
public void trackBySmell() {
System.out.println("trackBySmell");
}
}
class DogBoy extends CanineHero<SuperHearSmell>{
public DogBoy() {
//子类可以是默认构造器,只要调用了父类的构造器即可,表示能构造出父类来
super(new SuperHearSmell());
}
}
通配符
在创建多态数组时,该数组只能存放具体的子类型的数组的类型,如 Fruit[] f = new Apple[10]; 只能存放Apple及其子类的类型,否则虽然编译期可以通过,运行时却会报错。
通配符引用的是明确的类型,因此下面这个例子里(<? extends Fruit>)的通配符意味着该引用没有指定的具体类型。这种情况下该泛型容器设置不能添加Object类型的值。
使用泛型容器必须指定具体的泛型容器类型。
@Test
public void fruitTest() {
List<? extends Fruit> list = new ArrayList<Apple>();
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Apple)
//list.add(new Apple());
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Fruit)
//list.add(new Fruit());
//list.add( new Object());
list.add(null);
}
<? extends Fruit> 只能确认get获得的值的有效边界为Fruit类型,不能确认set设置的值,因为这样意味着他可以是任何事物,比如Object也可以设置,因为它是最基本的父类,这样编译器就无法验证“任何事物”的类型安全性,因此无法set。
超类型通配符:<? super T> 或 <? super Apple> 通过超类型通配符使我们可以确定边界是Apple,该容器的存放的元素都是Apple及其子类,因为适配的最大父类类型就是Apple。
无界通配符:<?>
一般用于如声明这段代码是用java的泛型来编写,并不是要用原生类型编写;还有就是在处理多个泛型参数时,通过无界通配符来运行一个参数是任意类型的,而其他参数为特定类型,这种使用场景特别重要。
如Map<String, ?> map = new HashMap<String, ?>;
使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。
泛型问题
任何基本类型都不能作为类型参数,但可以使用基本类型的包装类来作为泛型类型参数。
注意:自动包装机制不能应用于数组。
一个类不能实现同一个泛型接口的两种变体(如一个类实现了该泛型接口又继承了实现了这个泛型接口的类),由于擦除的原因,这两个变体会成为相同的接口。(去掉泛型参数便可以了)
转型和警告
使用带有泛型类型参数的转型或instanceof不会有任何效果。(因为运行时是类型擦除的)。
正确的转型方式是通过泛型类来转型:
List<String> list = List.class.cast(in.readObject());
自限定的类型
?
动态类型安全检查
通过Collections.checkedList()这些工具类方法使得List、Map、Set等这类集合可以安全地存入泛型类型的元素。
@Test
public void fruitTest() {
List<Apple> list1 = new ArrayList<Apple>();
add(list1);//正常运行,只有把Orange从容器取出时才会报错。
List<Apple> apples = Collections.checkedList(new ArrayList<Apple>(), Apple.class);
add(apples);//报错
}
异常
由于检查型异常的缘故,将不能编写出泛化的代码。
混型
混型:值混合多个类的能力,以产生一个可以表示混型中所有类型的类。混型使组装多个类变得简单易行。
一般就是多重继承,在java这边是实现多个接口,然后在创建域创建出接口对应的实现类,在调用中通过代理调用域中实现类的方法,将方法调用转发给恰当的对象。
潜在类型机制
java的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。
java想实现类似潜在类型机制则编译器会强制要求这些类实现某个接口,以便通过泛型方法< ? extends Perform>来实现这种效果。
对前置类型机制的补偿
- 反射。通过反射可以在实现或继承任何接口的情况下调用其方法。
- 将一个方法应用于序列。(能够实现在编译器类型检查)
- 用适配器仿真潜在类型机制。
2 疑问
p390 末端哨兵怎么实现的,看不明白?
无论何时,只有你能做到,就应该尽量使用泛型方法。这是对的么?首先有个效率问题吧,如非需要适应多种不同类型,没有必要这样处理吧?
Java泛型是使用擦除来实现的,怎么理解?是指都编程Object类型?
类型标签?指的是ClassL类型的属性标签么?
工厂对象?指的是用于创建工厂的东西,比如class、字符串?
什么叫显式的工厂?
p416,成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。这句怎么理解?(数据泛型转型需重新看)
15.10通配符这块不知道其具体用处,工作中有用到吗?
p429例子待研究
15.16 潜在类型机制?Python和C++ 是支持潜在类型机制的语言实例。
泛型还需重新理解一遍,不够全面。
3 思想总结
泛型的最大用处便是在使用容器类集合时(如List、Map等)可以设置某种泛型版本的容器,而不会因为存取时向上转型为Object而需要向下强转。但这只是个方便的编译期检查且使用时减少了强转的代码量,实际上由于泛型是后面添加到java中的,导致泛型的实现为了兼容以前非泛型的代码而使用类型擦除来实现泛型,导致java无法在运行时获取真正的类型信息,无法实现更加泛化的代码。
十六、数组
1 知识
数组的基本认识:数组通过整型索引值访问元素,并且数组的尺寸不能改变。
数组为什么特殊
数组与其他种类的容器之间的区别有三方面:
- 效率。在java中,数组是效率最高的存储和随机访问对象引用序列的方式。(数组是个简单的线性序列)代价是数组对象的大小被固定(比如与ArrayList对比,因为ArrayList可以实现自动分配空间,这种弹性开销时其效率比数组低),且在其生命周期中不可改变。
- 类型。在泛型之前,数组可以创建某种具体类型通过编译期检查来防止插入错误类型,而其他容器不行。泛型后都可以了。
- 保存基本类型的能力。在泛型之前,数组可以持有基本类型,而其他容器不行。泛型后都可以了,因为有字段包装机制。
数组和ArrayList之间的相似性是有意设计的,这使得两者之间的切换比较容易。随着泛型和自动包装机制的出现,数组相比于容器的仅有优点便是效率,而一般情况下,容器比数组拥有更多的功能方法,且数组限制更多,所以一般优先使用容器。
数组标识符只是一个引用(数组是个对象),指向在堆中创建的一个真实对象,这个数组对象用以保存指向其他对象的引用。length方法(length是数组的大小,而不是实际保存的元素个数。)和“[]”语法是访问数组对象唯一的方式。
对象数组与基本类型数组的区别是对象数组保存的是引用(默认初始化为null),而基本类型数组直接保存基本类型的值(按各基本类型的值初始化,如布尔型是false,int是0)。
数组中构成矩阵的每个向量都可以具有任意的长度(这被称为粗糙数组)。
数组与泛型(待重新了解)
数组与泛型不能很好地结合,不能实例化具有参数化类型的数组,我们不能创建泛型数组。因为擦除会移除参数类型信息,而数组必须知道它们所持有的确切类型,以强制保证类型安全。但是我们可以参数化数组本身的类型。
一般而言,泛型在类或方法的边界处很有效,而在类或方法的内部,擦除通常会使泛型变得不适用。
//作用十分有限,只能用同一个值填充各个位置,针对对象而言,就是复制一个引用进行填充。
int[] t = new int[5];
Arrays.fill(t,3);
System.out.println(Arrays.toString(t));
Arrays.fill(t,3,5,5);//填充下标3-4的值
System.out.println(Arrays.toString(t));
//复制数组
//注意复制对象数组时,复制的是对象的引用,而不是对象本身的拷贝,是浅复制。
//System.arraycopy(t,0,y,0,y.length);
//参数依次是:源数组,从源数组的什么位置开始复制的偏移量,目标数组,从目标数组的什么位置开始复制的偏移量,复制元素个数
System.arraycopy(t,0,y,0,y.length);
System.out.println(Arrays.toString(y));
数组的比较
//用来比较整个数组,比较数组元素个数和对应位置上的元素是否相等。注意比较的是元素的内容,不是位置。
Arrays.equals(y,t);
数组元素的比较
java有两种方式提供比较功能:
- 实现 java.lang.Comparable接口,重写compareTo()方法。
- 创建一个实现了Comparator接口的单独的类。传入Comparator比较对象。
Arrays.sort(u,Collections.reverseOrder());
针对已排序的数组,可以使用Arrays.binarySearch(t,5);查找元素的位置。如果查不到,返回负值,表示若要保持数组的排序状态次目标元素所应该插入的位置。负值的计算方式:-(插入点)-1
插入点指(以从小到大排序为例),第一个大于查找对象元素在数组中的位置,如果数组中所有的元素都小于要查找的对象,也就是说查找的值是最大的,那么插入点就是a.size().
@Test
public void arrayTest() {
int[] a = {1, 2, 3, 4, 5};
int[] b = a.clone();
int[] c = a;//两个引用指向同一个数组对象
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
System.out.println(a.equals(b));//false
System.out.println(a == b);//false
System.out.println(a.equals(c));//true
System.out.println(a == c);//true
int[][] aa = {{1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}};
//Arrays.deepToString(aa)用于多维数组展示
System.out.println(Arrays.deepToString(aa));
boolean[][][] flagArr = new boolean[3][3][3];
System.out.println(Arrays.deepToString(flagArr));
System.out.println(Arrays.deepToString(fillArray(2, 3, 4)));
int[] t = new int[5];
Arrays.fill(t, 3);
System.out.println(Arrays.toString(t));
Arrays.fill(t, 3, 5, 5);
System.out.println(Arrays.toString(t));
int[] y = new int[5];
//复制数组
System.arraycopy(t, 0, y, 0, y.length);
System.out.println(Arrays.toString(y));
System.out.println(Arrays.equals(y, t));
Arrays.sort(t);
Integer[] u = new Integer[5];
Arrays.fill(u, 6);
u[2] = 7;
u[4] = 1;
Arrays.sort(u, Collections.reverseOrder());
System.out.println(Arrays.toString(u));
System.out.println(Arrays.binarySearch(t, 5));
//如果使用Comparator排序某个对象数组(基本类型数组无法使用Comparator),在使用binarySearch()时必须提供同样的Comparator。
System.out.println(Arrays.binarySearch(u, 1, Collections.reverseOrder()));//4
//因为-1小于所以的值,对于颠倒排序来说,-1就是数据里面最大的,应该是-size-1 = -6
System.out.println(Arrays.binarySearch(u, -1, Collections.reverseOrder()));//-6
}
2 疑问
数组是个简单的线性序列,什么叫线性序列?为什么线性序列的元素访问非常快速?
16.6 忽略了创建测试数据这节。
为什么未排序数组上执行二分法查找结果是不可预知的?我测试结果是可知不变的,不过是错误的。
@Test
public void array1Test() {
int[] a = {1, 2, 3, 4, 5, 8, 1, 3, 2};
//Arrays.sort(a);//7
int i = Arrays.binarySearch(a, 3);
System.out.println(i);
}
3 思想总结
优先选择容器而不是数组,只有当性能成为问题且切换到数组对性能提高有所帮助时,才应该重构成数组。
十七、容器深入研究
针对容器这块有很多疑问,肯定要经过二次过滤学习,所有不懂的可以先记下来跳过。回过头来再看。
1 知识
集合类库图
填充容器
所有的Collection 子类型都有一个接收另外一个Collection 对象的构造器,用所接收的Collection 对象中的元素来填充新的容器。
通过继承Abstract容器类来定制Collection 和Map实现。
享元模式:在普通的解决方案需要过多的对象,或者产生普通对象太占用空间时使用享元。
Collection的功能方法
注意:Collection的方法不包括随机访问的get()方法。因为Collection包含Set,而Set是自己维护内部顺序的。
//List 的 <T> T[] toArray(T[] a);方法用法
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//不能强转,要用上面的泛型方法
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
可选操作
最常见的未获支持的操作,都来自于背后由固定尺寸的数据结构支持的容器。
应该把Arrays.asList(array1)的结果作为构造器的参数传递给任何Collection(或者addAll()方法或Collections.addAll()方法), 这样可以生成允许使用所有的方法的普通容器
//以下两个都不支持修改List容器的操作,会抛出UnsupportedOperationException异常
//他们的唯一的区别是Arrays.asList(array1)支持set()方法,因为它不会改变该数组的长度,该List是基于一个固定大小的数组,
// 仅支持那些不会改变数组大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了所以的修改操作,所以不行。
//任何会引起对底层数据结构的尺寸进行修改的方法都会抛出UnsupportedOperationException异常,
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
List的功能方法
public static void main(String[] args) {
System.out.println(capitals().get("CHINA"));
System.out.println(names());
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
//以下两个都不支持修改List容器的操作,会抛出UnsupportedOperationException异常
//他们的唯一的区别是Arrays.asList(array1)支持set()方法,因为它不会改变该数组的长度,该List是基于一个固定大小的数组,
// 仅支持那些不会改变数组大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了所以的修改操作,所以不行。
//任何会引起对底层数据结构的尺寸进行修改的方法都会抛出UnsupportedOperationException异常,
// 因此,应该把Arrays.asList(array1)的结果作为构造器的参数传递给任何Collection(或者addAll()方法或Collections.addAll()方法,
// 这样可以生成允许使用所有的方法的普通容器)
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
//判断容器内是否有该元素,有则返回该元素所在第一个位置,没有返回-1
int sudan = list1.indexOf("SUDAN");
//返回最后出现该元素的位置,没有返回-1
int sudan1 = list1.lastIndexOf("SUDAN");
//判断容器是否为空,没有存任何元素,如果容器中添加了null也算是元素,会返回false.
boolean empty = list1.isEmpty();
List<String> list3 = new ArrayList<String>();
list3.add(null);
boolean empty1 = list3.isEmpty();//false
System.out.println(empty);
ListIterator<String> listIterator = list1.listIterator();
//传入index参数则会把游标cursor从0移到参数为3迭代,也就是说只迭代从第四个元素开始的容器元素。
ListIterator<String> listIterator1 = list1.listIterator(3);
System.out.println(listIterator.next());
System.out.println(listIterator1.next());
list3.add(null);
list3.add(null);
//可以通过下标参数移除元素,返回该下标位置上元素的值(从0开始),如果传入的index下标超过
// list长度则会抛IndexOutOfBoundsException
String remove = list3.remove(0);
//也可以通过想要移除的具体元素来移除容器中这个值的元素,如果有则移除返回true,没有返回false,
// 多个相同元素只移除第一个匹配到的元素(一般都是从0到list.size()开始匹配)
boolean b = list3.remove(null);
System.out.println(b);
//修改容器指定位置的值
list3.set(0,"1");
list3.add("2");
list3.add("1");
list3.add("1");
list3.add("5");
ArrayList<String> list4 = new ArrayList<>();
list4.add("1");
//保留list3中两个List的交集
boolean b1 = list3.retainAll(list4);
System.out.println(list3);
//移除list3中所有与list4元素值相同的元素,不论有多少个相同的全部移除。
boolean b2 = list3.removeAll(list4);
System.out.println(list3);
//移除容器中所有的元素,包括null元素。
list3.clear();
System.out.println(list3);
}
Set和存储顺序
如果没有其他的限制,默认选择HashSet,因为它对速度进行了优化。
在使用容器时,必须为散列存储和树型存储都创建或者重写一个equals()方法。hashCode()方法只有在这个类被置于HashSet或者LinkedHashSet中时才是必需的。
良好的编程风格要求我们在覆盖equals()方法时,总是同时要覆盖hashCode()方法。
SortedSet (被TreeSet实现)
SortedSet 中的元素可以保证处于排序状态(按对象的比较函数进行排序)。
如果要按插入顺序排序其实就是LinkedHashSet.
@Test
public void sortedSetTest() {
SortedSet<String> set = new TreeSet<>();
Collections.addAll(set,"all man are create equal".split(" "));
System.out.println(set);
System.out.println(set.first());
System.out.println(set.last());
System.out.println(set.comparator());
Iterator<String> iterator = set.iterator();
String low = null;
String high = null;
for (int i = 0 ;i<4;i++){
if (i==1) {
low = iterator.next();
}
if (i==3) {
high = iterator.next();
} else {
iterator.next();
}
}
//截取set,包括low,不包括high
System.out.println(set.subSet(low,high));
//截取set小于high
System.out.println(set.headSet(high));
//截取set大于等于low
System.out.println(set.tailSet(low));
}
队列
LinkedList和PriorityQueue的差异仅在于排序行为而不是性能。
普通队列一般是先进先出的。ArrayBlockingQueue、ConcurrentLinkedQueue、LinkedBlockingQueue、PriorityBlockingQueue这些除了最后一个优先级队列,都是按照元素进入队列的先后顺序拿到的。
双向队列
双向队列还是一个队列,但是我们可以在任何一端添加或移除元素。可以通过LinkedList创建双向队列。
理解Map
映射表(也称关联数组)的基本思想是它维护的是键值对,因此你可以使用键来查找值。
HashMap | TreeMap | LinkedHashMap | WeakHashMap | ConcurrentHashMap | IdentityHashMap .
以上的Map行为特性各个不同,表现在效率、键值对的保存和呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定“键”等价的策略等方面。
性能
当在get()中使用线性搜索(for循环遍历)时,执行速度会相当的慢,HashMap使用了散列码来取代对键的缓慢搜索。散列码是“相对唯一”的,用以代表对象的int值。
Map的键必须是唯一的,而值可以有重复。因此键keySet返回的是Set;而值是Collection。
TreeMap是SortedMap目前的唯一实现,如果没有传或者值没有comparator,则默认是自然排序。键值对是按键的次序排列的。
//accessOrder = TRUE 表示采用基于访问的LRU算法
LinkedHashMap<String,String> map = new LinkedHashMap(16,0.75F,true);
LinkedHashMap 是以插入的顺序进行遍历的,基于LRU算法的版本也是如此。但是,在LRU版本中,访问越少的元素会出现在队列的最前面。
散列与散列码
Object的hashCode()使用对象的地址计算散列码;equals()比较对象的地址。因此,如果要使用自己的类作为HashMap的键,那么就要重写equals()和hashCode()方法。
正确的equals()必须满足以下5个条件:
自反性 x.equals(x)=true
对称性 y.equals(x)=true→x.equals(y)=true
传递性 x.equals(y)=true,y.equals(z)=true→x.equals(z)=true
一致性 无论比较对少次,结果都是一样的
x!=null→x.equals(null)=false
注意:Map.Entry是一个接口,用来描述依赖于实现的结构,因此如果你想要创建自己的Map类型,就必须同时定义Map.Entry的实现。
为速度而散列
线性查询是最慢的查询方式。
对查询速度再进一步的解决方案是保持键的排序状态,然后使用二分法进行查询。Collections.binarySearch()。
散列的价值在于速度,散列使得查询得以快速进行,它将键保存在某处,以便能够很快找到。
存储一组元素最快的数据结构是数组,使用数组来表示键的信息(散列值,作为数组的下标)。
为解决数组容量被固定的问题,不同的键可以产生相同的下标。当键相同时,则比较内容。
查询一个值的过程首先是计算散列码,然后使用散列码查询数组。如果没有冲突,则是个完美的散列函数;如果冲突了,则冲突通过外部链接处理。数组不直接保存值,而是保持值的List,如LinkedList,然后对list值使用equal方法进行线性查询。(虽然线性查询最慢,但是因为散列得好的话,每个位置只有较少的值,这样就不太好影响效率)
散列表的”槽位“(slot)通常称为桶位(bucket)。
为使散列分布均匀,一开始是使用质数,后面发现质数并不是散列桶的理想容量,后面改用2的整数次长度的散列表,用掩码代替除法。求余的%操作是开销最大的操作(因为get()是使用最多的)。
生成hoshcode()方法参考如下:
选择不同的容器实现来使用
容器之间的区别通常归结为由什么样的数据结构来实现它们。
虽然有些容器接口相同会拥有共同的操作,但操作的性能却并不相同,在选择使用哪个时可以基于某个特定操作的频率和执行速度来进行选择,一般用性能测试来选择。
P538的性能测试demo可以学习一下。
结果:使用时最佳的做法是将ArrayList作为默认首选,只有当需要额外的功能,如经常从表中间插入和删除时才去选择LinkedList。
HashSet的性能基本上总是比TreeSet好,优先使用,除了当我们需要一个排好序的set时,才应该使用TreeSet,
TreeSet迭代通常比HashSet要快。插入操作,LinkedHashSet比HashSet代价要高是因为LinkedHashSet还要维护链表(保证插入顺序)。
优先使用HashMap,只有在要求Map始终保持有序时,才需要使用TreeMap。LinkedHashMap比HashMap代价要高是因为LinkedHashMap还要维护链表(保证插入顺序)。
HashMap的性能因子
HashMap使用的默认负载因子是0.75。即尺寸/容量。 尺寸值表中当前存储的项数,容量指表中的桶位数。HashMap可以指定初始容量。
java容器类类库采用快速报错(fail-fast)机制。一旦发现容器上有任何除了当前进行锁进行的操作以外的变化就会抛出ConcurrentModificationException异常。
持有引用
软引用 SoftReference 、弱引用 WeakReference、虚引用 PhantomReference。对象的可获得程度由强到弱。使用这些类为垃圾回收提供了更大的灵活性。
使用前提:想继续持有对某个对象的引用,但也希望能够允许垃圾回收器释放它。这样我们便可以继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。
使用方法:使用Reference对象作为调用者与普通引用之间的包装(代理),且不能有其他普通的引用指向那个对象,这样才能达到上面的目的。(继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。)
如果引用的是个普通的引用,则该对象是可获得的,那么垃圾回收器就不能释放它。
SoftReference 用以实现内存敏哥的高速缓存。
WeakReference 为了实现规范映射而设计的。
PhantomReference 用以调度回收前的清理工作,它比java终止机制更灵活。
注意:使用SoftReference 和 WeakReference 可以选择是否要将它们放入ReferenceQueue队列。(回收前清理工作的工具)。而PhantomReference 只能依赖于ReferenceQueue。
552
2 疑问
我个人认为,容器的深入研究这一章不需要勉强自己第一遍就要看懂,看透,但需要多研究几遍,直到看透了,理解了基本常用的这些容器底层原理。
什么是掩码?
散列机制是如何工作的?
使用散列容器时如何编写hashCode()和equals()方法?
为什么某些容器会有不同版本的实现?如何选择?
17.2.1 的generator?
p494 中LinkedHashSet 的添加操作是不是重复添加了一遍?
容器类的打印是不是都是默认toString就行?
Map.Entry<String, String> 里的这个Entry是个什么?
Map.Entry是一个接口,用来描述依赖于实现的结构,因此如果你想要创建自己的Map类型,就必须同时定义Map.Entry的实现?
内部类在Map等容器里的使用?
Map的内部实现和构造?
P525练习?
17.2重新研究一下
动态语言可以在任何对象上调用任何方法,并且可以在运行时发现某个特定调用是否可以工作。动态语言和静态语言的区别?对比?研究一门动态语言自我感觉与静态语言的对比?
List的内部结构和底层?
什么叫链表,单向链表和双向链表?
list1.listIterator()的使用?
p510练习8.
散列存储和树型存储?指的是如HashSet和TreeSet么?
怎么自己重写equals方法 hashcode方法?怎么用idea重写?
HashMap的底层实现?其他Map的底层实现?
P517下面注释写的好处怎么解释?
使用数组代替溢出桶,有两个好处:
- 可以针对磁盘存储方式做优化。
- 在创建和回收单独的记录时,能节约很多时间。
最近最少使用(LRU)算法?一般用在什么场合?
3 思想总结
待续。。。。。。
其他
roy mustang 原来钢炼里的罗伊.马斯坦 是野马的意思。
面向函数式编程 :Scala 、Elm 。可以多去了解下函数式编程的语言,语言的目标是写出更简洁的代码,或者效率更高,或者你的代码更容易被大家懂 。
语言方面更多关心的议题 :第一,多产。第二,多线程的问题。第三,错误,如何发现跟错误相关的一些议题
每当我有问题需要被解决的时候我发现Python是最快可以给我结果的一个语言 。
为什么Python会作为机器学习非常好的一种语言,因为Python把其他语言做了一个封装,调用其他语言做的包。很多的数据科学家他们其实是不希望学习过于复杂的编程语言,能够把他们关于数据方面处理的智慧进行封装起来,通过Python来调用这样会方便很多,这也是为什么Python这几年这么流行的原因。
mysql排序问题:比如我们以创建日期来排序,当日期相同时,mysql会随机排序。
疑问:
有点意识到自己喜欢理论大而泛的模糊知识的学习,而不喜欢实践和细节的打磨,是因为粗心浮躁导致的么>
在我看来,其实是对知识的空洞和无知,导致了落实不到细节,因此只能宏观上了去感受和学习,落实不到细节上来,还不到粗心浮躁,而是基础太差,只能一步一个脚印,找个清单,从头开始学习起,就不会学习个新东西被俄罗斯套娃到另一个新东西再到另一个新东西上了,因为你懂的足够多,更容易理解这样东西或者技术。
参考文献
JAVA编程思想
建议不继续维护,整理新版本下的Java基础。