Java知识补充

回过头发现有些 Java 基础并没有学好,也许是忘了也许是学的时候就没仔细看,那就在这里补回来
本篇介绍的是:迭代器、可变参数、枚举、反射、注解、动态代理的简单使用

迭代器与for

Iterator迭代器

Java 提供一个专门的迭代器 Iterator,它是一个接口,我们可以对某个序列实现该 interface,来提供标准的 Java 迭代器。Iterator 接口实现后的功能是“使用”一个迭代器,比如定义了 next 等方法
也就是说,实现了这个接口就可以称为是一个迭代器了

Iterable可迭代对象

Java 中还提供了一个 Iterable 接口,Iterable 接口实现后的功能是“返回”一个迭代器,我们常用的实现了该接口的子接口有: Collection、Deque、List、Queue、Set 等,该接口的 iterator() 方法返回一个标准的 Iterator 实现。实现这个接口允许对象成为 For each 语句的目标。就可以通过 For each 语法遍历你的底层序列。
也就是说:实现这个接口允许对象成为 “foreach” 语句的目标,实现接口就成为了可以被迭代的对象。
Iterable 接口包含一个能够产生 Iterator 的 iterator() 方法,并且 Iterable 接口被 foreach 用来在序列中移动。因此如果创建了任何实现 Iterable 接口的类,都可以将它用于 foreach 语句中。

for each

forEach 不是关键字,关键字还是 for,语句是由 iterator 实现的,他们最大的不同之处就在于 remove() 等方法上。
但是,如果在循环的过程中调用集合的 remove() 方法,就会导致循环出错,因为循环过程中 list.size() 的大小变化了,就导致了错误。 所以,如果想在循环语句中删除集合中的某个元素,就要用迭代器 iterator 的 remove() 方法,因为它的 remove() 方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,例如,你不能连续两次调用它的 remove() 方法,调用之前至少有一次 next() 方法的调用。

forEach 就是为了让用 iterator 循环访问的形式简单,写起来更方便。当然功能不太全,所以但如有删除等操作,还是要用它原来的形式。

Iterator与for特点

采用 ArrayList 对随机访问比较快,而 for 循环中的 get() 方法,采用的即是随机访问的方法,因此在 ArrayList 里,for 循环较快

采用 LinkedList 则是顺序访问比较快,iterator 中的 next() 方法,采用的即是顺序访问的方法,因此在LinkedList 里,使用 iterator 较快

从数据结构角度分析, for 循环适合访问顺序结构,可以根据下标快速获取指定元素;而 Iterator 适合访问链式结构,因为迭代器是通过 next() 和 Pre() 来定位的,可以访问没有顺序的集合.

而使用 Iterator 的好处在于可以使用相同方式去遍历集合中元素,而不用考虑集合类的内部实现(只要它实现了 java.lang.Iterable 接口),如果使用 Iterator 来遍历集合中元素,一旦不再使用 List 转而使用 Set 来组织数据,那遍历元素的代码不用做任何修改,如果使用 for 来遍历,那所有遍历此集合的算法都得做相应调整,因为List 有序, Set 无序,结构不同,他们的访问算法也不一样.
清空 List 的时候(单条删除),除了使用迭代器,用 for 条件倒序删除就不会出现越界问题,想出这个方法的真是人才…

可变参数

可变参数完全可以当作数组来用,是在 Java5 加入的,如果有多个参数可变参数必须在最后
因为可变参数基本等同于数组,所以甚至可以直接传一个数组给他,关于可变参数在 API 文档中随处可见,比如我想到了 Arrays.asList() 这个方法,你可以传出多个参数也可以直接传入一个数组(这个方法接收的是对象参数,不要传基本类型的数组啊,要不然整个数组会被当作一个参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args){
int sum = testSum(1,2,3,4,5,6,7);
System.out.println(sum);
System.out.println(testSum(new int[]{1,2,3,4,5,6,7}));
}

private static int testSum(int ...nums) {
int sum = 0;
for (int i :
nums) {
sum += i;
}
return sum;
}

枚举

使用枚举的情况不算少,就是那种需要传入一些固定的参数的方法,比如一个方法允许传入的参数值就两种字符 A 和 B 吧,但是如果定义为 char 类型的话调用者可以瞎鸡巴传的,传个 D 、E、F 都是合法的,枚举就是用来做这个的
当然也可以自定义一个类,然后把构造方法私有了,内部制造出指定的几个,这样就太麻烦了,看下面定义个简单的枚举,相当于是个类

1
2
3
public enum TestEnum {
A, B, C, D, E;
}

使用起来非常简单,比如:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
enumTest(TestEnum.A);
}

private static void enumTest(TestEnum e) {
System.out.println("name --->" + e.name());
}
}

上面的是最最简单的使用,既然枚举相当于一个类,那么它也有构造函数,也有字段、方法,还可以定义抽象方法,不过要在声明枚举项的时候也就是上面的 A B C D 的时候给复写了,这样就可以直接通过 TestEnum.A.methodName() 的形式调用
具体的栗子代码放在了这里Github

反射

反射技术非常的重要,一般用于来做框架,虽然做框架都是大牛,但是不懂反射也看不懂框架啊,那就没法用啊,所以还是很有必要的,可以不用精通嘛
反射就是加载类,并解剖出类的各个组成部分;别人扔给你一个编译后的类,你不知道里面是啥东西吧,然后别人可能会告诉你里面有个什么方法,这时候想要执行就得用反射,首先把类给搞进去,弄出个对象来,然后检测有没有这个方法,有就调用呗

获取类的字节码文件,也就是把类加载到内存中去,一般有三种方式:

1
2
3
Class<Student> clazz = (Class<Student>) Class.forName("com.bfchengnuo.reflect.Student"); // 完整路径
//Class clazz = new Student().getClass();
//Class clazz = Student.class;

通过反射加载了类下一步就是创建对象了,也就是要获取构造函数了,另外还可以获得方法、字段等,反正啥都可以得到,这就反射的厉害之处
详细的反射不在这写了,在 Github 上有

内省

内省可以当作是反射技术中的一类特殊的情况,它是专门来处理 JavaBean 类的,Java 还提供了相关的 API
简单说就是:内省就是用来访问某个属性的 getter/setter 方法的

一般在 JavaBean 中会从 Object 中继承一个属性,Object 中有个 getClass 方法啦

关于内省 Java 提供的 API 中,最重要的是 Introspector 类,它确定了到底“省”谁,被省了以后就会把 bean 的信息封装到一个 BeanInfo 中去;下面基本就是执行其 set 方法填充数据,或者 get 方法获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IntrospectionException {
// 获取javabean的所有属性
//BeanInfo beanInfo = Introspector.getBeanInfo(Stu.class);
// 除去从 obj 继承的属性
BeanInfo beanInfo = Introspector.getBeanInfo(Stu.class,Object.class);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); // 得到属性描述符
for (PropertyDescriptor pd : pds) {
// 获得属性名
System.out.println(pd.getName());

// 相当于是拿到set方法
Method writeMethod = pd.getWriteMethod();
// 执行方法,填充数据
writeMethod.invoke(new Stu(),"loli");

// 拿到get方法
Method readMethod = pd.getReadMethod();
readMethod.invoke(new Stu(),null);
}
}

注解

注解的出现,就是为了代替配置文件(比如 XML)的。注解是给程序看的,所以如果想要用自定义注解,那就要必须让程序(编译器)看明白才行,关键字 @interface 意思就是定义一个注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 元注解,给注解加注解,分别是:指定保留域、作用在那、可继承
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MyAnnotation {
// 可以定义八种基本数据类型、字符串、其他注解、类、枚举;当然也可以是数组
String name();
int age();

// 可以配置缺省值
Class clazz() default String.class;

// value 是比较特殊的,只有一个属性 value 的话,赋值的时候可以省略属性名
String[] value();
}

这样定义后就可以使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
private static String name;

static {
try {
// 通过反射,获取注解的信息,首先获取到方法,这个是获取公共方法的....
Method method = Main.class.getMethod("test");
// 获取到注解信息
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
name = annotation.name();
System.out.println(name);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

@MyAnnotation(name = "loli", age = 12, value = {"test"})
public static void test() throws NoSuchMethodException {
// TODO: 2017/5/24
}
}

使用了注解虽然不需要再配置 XML 了,也非常的直观,但是,弊端也体现出来了,如果想要改动配置,必须要修改 java 文件….还得重新进行编译
高级的用法就是,有可能加了一个注解就拥有了一个对象,说到底还是通过反射技术来实现的,通过反射解析出创建那个类,以及相应的数据,然后再利用反射设置到这个变量上去就行了(注入对象)

完整代码参考:Github

动态代理

动态代理的意义在于:拦截对真实业务对象的访问 ;拦截以后可以干嘛呢,这个就看需求了,比如可以对方法进行增强啥的,就不需要使用包装模式了

动态代理是利用 JavaAPI 在内存中动态的构建代理对象,套路一般是从被代理者上抽取一个接口,这个接口在后面有大用处,也是 Java 的规定,相关的 API 在反射包下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 抽取的接口
public interface Loli {
void hug(String name);
String eat();
}

// 具体的被代理者
public class StandardLoli implements Loli {
@Override
public void hug(String name) {
System.out.println("(づ。◕‿‿◕。)づ");
}

@Override
public String eat() {
System.out.println("开吃....");
return "吃饱了";
}
}

然后就是代理者了,一般内部维护一个具体被代理的对象,然后通过 Java 自带的 API ( Proxy.newProxyInstance() ) 返回给调用者一个代理者,也就是一个接口,当调用这个接口的方法时,会自动携带相关参数到 invoke 方法,嗯?!就是回调,然后在这个方法中进行一些判断或者增强啥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StandardLoliProxy {
private StandardLoli mLoli = new StandardLoli();

public Loli createProxy() {
// 固定写法
// 1.第一个参数是类加载器,用当前的类获取就可以,或者 mLoli
// 2.第二个为被代理者的接口,是接口
// 3.第三个为核心,返回的代理调用时就是调用这个方法
// 在这个方法里判断是调用的那个方法,然后执行相应的处理
return (Loli) Proxy.newProxyInstance(StandardLoliProxy.class.getClassLoader(), mLoli.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equalsIgnoreCase("hug")) {
System.out.println("确认身份...");
if (args[0].equals("bfchengnuo")) {
return method.invoke(mLoli, args);
}
} else if (method.getName().equalsIgnoreCase("eat")) {
System.out.println("代理人...");
return method.invoke(mLoli, args);
}
System.out.println("其他方法不允许执行!");
return null;
}
});
}
}

如果把内部维护的对象设置为 Object(使用泛型可能会更好),当然返回值也要是 Object 了,那么就成为了一个通用的代理对象了;最后简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
public class MainTest {
public static void main(String[] args) {
StandardLoliProxy proxy = new StandardLoliProxy();
Loli loli = proxy.createProxy();

loli.hug("bfchengnuo");
System.out.println("------------------------");
String result = loli.eat();
System.out.println("----------"+ result +"--------------");
// loli.hashCode();
}
}

其实除了动态代理还有静态代理,静态代理需要实现和代理对象相同的接口,在内部维护一个代理对象的实例,然后最后调用的是代理的方法(也就是 proxy.xxx 的方式),在调用代理类的方法中会调用内部实例的方法,有点类似装饰模式了,不建议使用

List家族

两大支柱当然就是 ArrayList 与 LinkedList 了,以往我们都是说 ArrayList 采用连续存储随机查找、读取快,增删慢;LinkedList 采用链表,增删快,随机访问慢。
这样说确实没问题,不过实际上还是有点差别,主要是我们忽略了“定位”的时间来说的,如果算上这个时间,直观上 ArrayList 可以完胜了,当然是在性能上。

拜读了这个系列的文章,对我的认知产生了不小的冲击,原文地址
另外也可以看看我的笔记本里对应地址

确实有点出乎意料,按照这样的观点,LinkedList 性能上岂不是没有任何优势根据第二篇介绍, LinkedList 因为双向链表,添加元素可以两边查找,所以在中间插入最耗时,Array 当然是开头插入最耗时,因为越是靠前需要进行移动的元素越多;
然而,无论哪一种,作者测试都是 ArrayList 遥遥领先…..但是 LinkedList 的耗时其实全部都用在了定位元素上,真正的插入和删除只是修改几个链接,是不耗时的;
无奈 ArrayList 使用 native 提速后一般的增删和链表差不多了,甚至反超…..看来维护双向链表的成本确实很大啊~

PS:与上面的迭代器相关内容没有影响。

其他

所有的包装类都默认实现了 Serializable 接口
也就是说,如果你不确定传过来的值是 String 还是 Long 或者 Integer 等对象,就可以使用 Serializable 来接收了!

参考

迭代器与for相关

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~