忽略与主题无关的其他信息
extends的意思是扩展,即子类是父类的扩展。扩展的意义在于不需要重新造轮子,这也是继承的意义。
方法名、形参列表相同。
返回值类型和异常类型,子类需要小于父类。
访问权限,子类需要大于父类。
封装的意思是想让你看到的就让你看到,不想让你看见的就隐藏起来。优点是高内聚低耦合。
高内聚:封装细节,保证安全。
低耦合:简化使用,便于扩展。
Java使用访问修饰符实现封装
private:私有的,只能在同一个类中使用
default:默认的,可以在同一个包中使用
protected:受保护的,可以由子类使用
public:公开的,可以被所有的类访问
一般使用peivate修饰属性,但提供get/set方法供外界访问。
同一个方法调用,不同的对象可能会有不同的行为。例如同样是十一假期,有人会出去旅游,有人会宅在家里。
多态是方法的多态不是属性的多态。
多态的必要条件,继承、方法重写、父类引用指向子类对象。
如果我们需要控制某个类的次序并且这个类本身不支持排序,那么就可以建立一个类比较器来进行排序。
类比较器的只需要实现 java.util.Comparator 接口并实现对应的方法就可以了~
在JDK1.6中 String存储在jvm的PermGen中,并且限制大小。同时该区域还用于存储类信息。
在JDK1.7中 String存储在heap 中, 大小无限制。
序列化在Java中一般指对象的序列化。具体含义是将对象转换为字节序列即二进制的过程。在这个过程中不仅仅保留当前对象的数据,而且会以递归地形式保存引用的每个对象的数据。利用对象序列化可以实现对对象的”深复制”。即复制对象本身以及引用的对象本身。序列化一个对象就可以得到整个对象序列。而反序列化则是将字节序列恢复为对象的过程。
pojo implements Serializable
序列化通常有两种用途,都是将对象写入字节流中。
当需要把对象的字节序列永久的保存到硬盘上时,一般以文件的形式。
字节序列需要在网络上传输时。P:选择一种高效的序列化和反序列化方式可以提升整个架构的性能。
存储真正的对象,每个对象都包含一个与之对应的class信息,这个class信息的作用是得到操作指令。
特点:只有一个堆区,随着JVM的启动而创建,所有线程共享区域,因此不存放基本类型和对象引用,只存放对象本身。如果堆内存剩余的空间不足以创建对象,JVM会抛出OutOfMemoryError的错误。P:堆一般由程序员分配。
存放基本数据类型和引用数据类型。P:Strtus中的压栈出栈操作
特点:每个线程包含一个栈区,并且栈中的数据都是私有的。栈中的数据由编译器自动分配释放。当一段代码定义一个变量时,JVM就为该变量在栈中开辟空间,当该变量退出作用域时,JVM就释放该空间,这个空间就随时可以被其他变量使用。P:栈分为三个部分。基本类型变量区、执行环境上下文、操作指令区。
又名方法区,是所有线程共享的区域,存放所有的类信息(类名,类方法信息,类字段信息)和static修饰的变量。
特点:整个程序中永远唯一的元素。全局变量和静态变量都放一起。不同的是,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。
HashMap是HashTable的轻量级的非线程安全的实现。它们都完成了Map接口。采用的算法hash/rehash都大概一样,底层也都是采用数据加链表结构实现,所以性能上不会有很大差异。除了HashMap和HashTable,Hash集合中还有一个HashSet,它里面存储的并不是key-value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,内部采用HashMap实现,所有的value都是同一个Object,只是包含HashMap中的key而已。
Java提供了两种不同的类型,数据类型和引用类型,int是Java的数据类型,Integer是Java的引用类型。不仅是int,Java为每个基本类型都准备了对应的引用类型。它们的行为和语义都不同。
String是作为常量在Java中被定义的,而StringBuffer是变量,意味着它可以修改对象内容。如果需要经常对一个字符串进行修改那么就可以考虑使用StringBuffer,因为它不会生成新的对象。
JDK1.5新增了一个可变的字符序列StringBuilder,它与StringBuilder有兼容但不同步的API,是StringBuilder的一个简单替换,用于字符串缓冲区被单个线程使用的时候,Java建议优先使用StringBuilder,因为在大多数实现中,它比StringBuffer速度要快,并且使用方法基本相同。
非运行时异常指的是编译时异常或者说受检异常,Java编译器强制要求这类异常在代码编写的时候就处理,或抛出或捕获。运行时异常指的是虚拟机正常运行中可能会遇到的异常。
*Java.lang.NullPointerException
程序遇上了空指针,简单地说就是调用了未经初始化的对象或者不存在的对象,这个错误经常出现在创建图片(路径),使用数组(初始化)中。
类不存在,注意检查类的名称和路径是否正确
数组越界访问
方法参数错误
单例模式的特点
单例模式中的类只能有一个实例
单例类必须自己创建自己的实例
单例类必须给其他对象提供实例
在计算机系统中,线程池、端口、缓存、日志、对话框、打印机、显卡的驱动程序常常被设计成单例模式,这主要是为了避免发生诸如两台计算机同时打印时打印机该如何工作的问题,还有就是端口冲突的问题。再例如听歌,一般音乐软件同一时间只能播放一首歌。,虽然你有两个耳朵,但是这不代表你开心同时听两首歌~
单例模式有两种表现形式
// 第一种形式
Class Singleton {
Private Singleton(){}
Private static Singleton instance = new Singleton();
// 提供一个供外界访问本类的静态API
Public static Singleton getInstance(){}
}
// 第二种形式
Class Singleton {
Private static Singleton instance = null;
// 创建静态访问器,为了防止两个线程同时进行对象的创建,加上同步锁
Public static synchronized Singleton getInstance(){
If (instance == null) {
Instance = new Singleton();
}
Return instance;
}
}
Factory(工厂模式)
Factory Method(工厂方法模式)
Prototype(原始模型模式)
Singleton(单例模式)
Adapter(适配器模式)
Decorator(装饰模式)
Proxy(代理模式)
Observer(观察者模式)
工厂模式是最常用的模式之一,根据工厂模式实现的类可以根据提供的数据生成一组类中的某一个类的实例,通过这一组类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作。首先需要定义一个基类,该类的子类通过不同的方法实现了基类中的方法,然后需要定义一个工厂类,工厂类可以根据条件生成不同的子类实例。当得到子类实例侯,开发人员可以调用基类的方法而不必考虑到底返回哪一个子类的实例。
工厂模式是Java中最常用的模式,原理是利用Java反射机制和多态的特性。目的是让程序更灵活,可以使项目并行开发,其中的粘合剂就是接口和配置文件。
Interface InterfaceTest{
Public void getName();
}
// 有了接口就可以根据接口
进行并行开发
// 程序员A
Class Test1 implements InterfaceTest{
Public void getName(){
System.out.println(“test1”);
}
}
// 程序员B
Class Test2 implements InterfaceTest{
Public void getName(){
System.out.println(“test2”);
}
}
// 工厂类 生产接口对象
// 在调用时得到的是接口对象,一个接口变量指向实现该接口的类对象
// 通过键获取值,而不是类的全路径
Class Factory{
Private static Properties pro = new Properties();
Static {
try {
// 加载配置文件
Pro.load(new FileInputStream(“xxx.xxx”));
} catch (Exception e) {
e.printStackTrace();
}
}
private static Factory factory = new Factory();
private Factory(){};
Public static Factory getFactory() {
return factory;
}
Public InterfaceTest getInterface() {
InterfaceTest interfaceTest = null;
try {
// 根据key,获取value value为类的全路径
String classInfo = pro.getProperty(“name”);
// 利用反射生成class对象
Class c = Class.forName(classInfo);
Object obj = c.newInstance();
interfaceTest = (interfaceTest )obj;
}catch (Exception e) {
e.printStackTrace();
}
return interfaceTest ;
}
}
// 调用方法
class FactoryTest {
public static void main(String[] args) {
Factory factory = Factory.getFactory();
// 通过创建的实例调用获得接口对象的方法获取接口对象
InterfaceTest inter = factory.getInterface();
// 调用接口定义的方法
Inter.getName();
}
}
ArrayList和Vector都使用数组的方式存储数据。当数组元素大于实际存储的数据时以便增加和插入元素,它们都允许直接按序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据块而插入数据慢,Vector由于使用了synchronized方法保证线程安全,因此性能上较ArrayList差。
LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引速度慢。因为插入数据时只需要记录本项的前后项即可,插入速度较快。
Collection是集合类的上级接口,继承于它的接口主要是Set和List。
Collections是针对集合类的一个帮助类,提供了一系列静态API,实现了对集合的搜索、排序、线程安全化等操作。
final用于声明属性、方法和类,分别表示该属性不可变、该方法不可覆盖、该类不可继承。
finally是异常处理结构语句的一部分,表示该代码块中的代码总是执行。如果是断电或者强行中断程序,则该块中的代码才不会被执行。
finalize是Object类中的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾回收时其他资源的回收,例如关闭文件。
wait()使一个线程处于等待状态,并释放所持有对象的lock.
sleep()使一个正在运行的线程处于睡眠状态,是静态方法,调用此方法要捕捉InterruptedException异常。
notify()唤醒一个处于等待状态的线程。注意是在调用此方法时,并不能确切知道唤醒的是那个等待状态的线程,也不是按优先级,而是由JVM确定唤醒的线程。
allnotity()唤醒所有处于等待状态的线程,注意不是给所有唤醒线程一个对象的锁,而是让它们竞争。
方法的重载和重写是Java多态的表现。前者是父子类之间,后者是同一个类之中。如果说子类中定义某方法与父类中的方法名称和参数都相同的话,我们就说该方法被重写了。如果在同一个类中定义了多个同名的方法,它们有不同的参数个数和参数类型,则称之为方法的重载。
两者是Java面向对象的重要实现。声明方法的存在而不去实现它的类叫做抽象类,它的子类必须全部实现它父类的所有方法,否则它也是一个抽象类。抽象类不能有抽象构造函数和抽象静态方法。也不能被实例化,并且它只能被public、protected修饰符修饰。
接口是抽象类的变体。在接口中所有方法都是抽象的,并且它只可以定义static final的成员变量。Instanceof运算符可以用来决定某对象的类是否实现了接口。
如果数据将在线程间共享,正在写的数据以后或者已经可能被另一个线程读到,那么这些数据就是共享数据,就必须使用同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,例如下载操作,这个时候不希望程序等待方法的返回,你在下载音乐的同时也可以听音乐,这个时候就需要使用异步编程。类似这种场景下异步途径往往更具有效率。
内存处理是程序员最容易出错的地方,忘记或者错误的内存回收会导致堆栈溢出系统崩溃等严重错误。Java并没有提供显示的释放已分配内存的操作方法,而是提供GC功能,用于自动检测对象是否超出作用域从而达到自动回收内存的目的。这个功能被称之为垃圾回收机制。
优点是程序员不再需要考虑内存管理,同时垃圾回收可以有效地防止内存泄露,有效地使用可用的内存。
缺点则是垃圾回收器作为一个单独的低级别线程运行,在不可预知的情况下对内存已经死亡或者长时间没有使用的对象进行清除和回收,程序员不能监测到异常情况,也不能实时调用垃圾回收器对对象进行垃圾回收。程序员可以手动调用System.gc(),但java语言规范并不保证GC一定会运行。
垃圾回收的机制有:分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
start()使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行,但并不意味着线程就会立即执行。此时线程处于就绪状态。
run()线程体,包含要执行线程的内容,当线程得到CPU的时间片,就会开始执行相应线程的此方法。
使用start()开辟线程,使用run()执行线程。
线程的出现是为了更好的利用CPU,提高程序运行效率。
使用线程池对线程进行统一分配,调优,监控可以降低资源消耗,提高相应速度,提高线程的可管理性。
J2EE服务器会在启动时建立若干线程池连接,并一直维持不少于此数目的池连接,当客户端程序需要连接时,池连接程序会返回一个未使用的池连接并标记为忙。如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量由配置参数决定,当使用的池连接调用完成后,池驱动程序将池连接标记为空闲,其他调用就可以使用这个连接。
forward转发是服务器请求资源,服务器直接访问目标地址的URL,将URL的响应内容读取过来,然后将这些内容再发送给浏览器,浏览器因此并不知道服务器发送的内容是从哪里来的,所以它的地址栏还是原来的地址。
redirect重定向是服务端根据业务逻辑,发送一个验证码,告知浏览器去请求某个地址,一般来说,浏览器会用刚才请求的参数重新请求,所以session、request参数都可以获取到。
Set里的元素不能重复,那么可以使用iterator()方法区分是否重复。
equals()是判断两个Set是否相等。属于深度比较,为的是当两个分离的对象的内容和类型相匹配的话返回ture。而”==”是比较两个对象的引用是否指向同一个内存地址,也就是判断对象是否分离。
hashCode()通常被设计用于提高性能。它和equals()的区别就在于当两个对象相等(equals()),那么它们就一定拥有同样的哈希值,但如果两个对象的哈希值相等,则不一定代表这两个对象就一定相等。
当Java程序违反了java的语义规则时,虚拟机就会将发生的错误表示为一个异常。违反语义包括了两种情况。
Java类库内置的语义检查。如果数组下标越界则会引发IndexOutBoundsException;访问null对象时会引发NullPointerException。
另一种是程序员扩展的语义检查,自定义异常并自由选择在何时使用throw关键字引发异常。所有自定义的异常都必须继承java.lang.Thowable。
两种形式:dtd和schema
数据类型定义,用于描述XML文档的文档结构,是早期的XML文档定义形式。
本身基于XML编写,在类型和语法的限定能力上比dtd强,处理也方便,因此正逐渐代替dtd成为新的模式定义语言。
本质区别:schema本身是xml,可以被xml解析器解析。(这也是从DTD上发展schema的根本原因)
解析方式:DOM、SAX、STAX等
处理大型文件时性能下降厉害,这个问题是由DOM的树结构造成的,这种结构占用的内存较多,而且DOM必须在解析文件之前把整个文档都装进内存,便于对XML的随机访问。
事件驱动型的XML解析方式。它按顺序读取XML文件,而不需要一次全部装载整个文件。当遇到像文件开头,文档结束或者标签开头与标签结束时会触发一个事件,用户通过在其回调事件中写入处理代码来处理XML文件,适合对XML的顺序访问。
Streaming API for XML(StAX)
Public void init(ServletConfig config);
Public ServletConfig getServletConfig();
Public String getServletInfo();
Public void service(ServletRequest request, ServletResponse response);
Public void destroy();
- init()方法在servlet生命周期中仅执行一次,在服务器装载servlet时执行。缺省的init()通常是符合要求的,不过也可以根据需要override,比如管理服务器端资源,一次性装入Gif图像,初始化数据库连接等。由于缺省的init()中设置了servlet的初始化参数,并使用了ServletConfig对象参数来启动配置,所以覆盖init()时,需要调用super.init()以确保仍然执行这些任务。
- service()是servlet的核心方法,在调用service()之前,应确保已完成了init()。对于HttpServlert,每当用户请求一个HttpServlet对象,该对象的service()就会被调用,HttpServlet缺省的service()中的服务功能就是调用与HTTP请求的方法对应的do功能,所以对于HttpServlet,一般都重写doPost()或者doGet()。
- destroy()在servlet的生命周期中也仅执行一次,在服务器停止并卸载servlet时执行,将servlet作为服务器进程的一部分进行关闭。缺省的destroy()是符合要求的,但也可以override。比如在卸载servlet时将统计的数字保存在文件中,比如关闭数据库连接。
- getServletConfig()返回一个servletConfig对象。该对象用来返回初始化参数和servletContext。servletContext接口提供有关servlet的环境信息。
- getServletInfo()提供有关servlet的信息,比如作者、版本、版权。
#throw和throws
>throw语句用来明确地抛出一个异常
>throws用来表明一个成员函数可能抛出的各种异常
#排序有哪几种方法?请列举并口述用java实现快速排序。
>排序的方法有
* 插入排序(直接插入排序,希尔排序)
* 交换排序(冒泡排序,快速排序)
* 选择排序(直接选择排序,堆排序)
* 归并排序
* 分配排序(箱排序,基数排序)
>快速排序的伪代码
从a[0:n-1]中选择一个元素作为middle,该元素为支点。
将余下的元素分割为left和right两段。使left中元素都小于等于支点,而right中的元素都大于等于支点。
递归地使用快速排序方法对left进行排序,然后是right
所得出的结果为left+middle+right;
#MVC的各个部分都有那些技术实现?怎么实现的?
- Model
代表着业务逻辑,通过JavaBean实现
- View
是应用的表现层,用于用户交互,使用JSP页面技术实现
- Controller
应用程序处理过程控制,一般是Servlet
>优点:开发效率高;程序灵活性和扩展性好;代码重用度高;便于人员分工。
>缺点:代码复杂度增加;代码数量增加;不适合开发小型程序。
#java中有几种方法可以实现一个线程?用什么关键字修饰同步方法?Stop()和suspend()为何不推荐使用?
>有两种方法,分别是继承Thread类和实现Runnable接口。
>使用关键字synchronized修饰同步方法
>反对使用stop()是因为不安全。它会接触由线程获取的所有锁定,而且如果对象处于一种不连贯的状态,那么其他线程能在那种状态下检查和修改他们,结果很难检查出真正的问题所在.suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获取的锁定,此时,其他线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们都想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便使用wait()命令其进入等待状态,若标志指出线程应当恢复,则使用一个notify()重新启动线程。
#java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?
>字节流、字符流。字节流继承InputStream OutputStream
字符流继承于InputStreamReader OutputStreamWriter
>在java.io包中还有许多其他的流,主要是为了提高性能和使用方便
#内部类可以引用他包含类的成员吗?有什么限制?
>一个内部类对象可以访问创建它的外部类对象的内容。
>内部类如果不是static,那么它可以访问创建它的外部类对象的所有属性。如果是static,即为nested class,那么它只可以访问创建它的外部类对象的所有static属性。
>一般类只有public或者package等访问修饰符。而内部类可以实现static、protected、private等访问修饰符。
>当从内部类继承的时候内部类是不会被覆盖的,它们是完全独立的实体,每个都在自己的命名空间内,如果从内部类中明确地继承,就可以覆盖原来内部类的方法。
#进程和线程的区别
+ 进程
是相对于操作系统而言的。例如你一边听歌一边玩游戏,我们就会说此时系统内有两个进程在运行,专业的话说叫做多个程序几乎在同一时间执行多个任务。
+ 线程
相对某一程序而言。例如你一边听歌一边看歌曲排行榜,还可以下载歌曲,这三件事情互相不会干扰,就可以说这一音乐程序至少有三个线程在运行,专业的表述为一个程序在同一时间内执行多个任务。
#你能说出TCP/IP的七层协议吗?
>应用层,、表示层、会话层、传输层、网络层、数据链路层、物理层。
#在connection类中提供了3个控制事务的方法,说说它们。
- setAutoCommit(boolean bln)
保持数据的完整性。一个系统的更新操作可能要涉及多张表,需要多个SQL语句进行操作,循环连续的进行插入操作,如果在开始时设置了”conn.setAutoCommit(false);”,然后再进行”conn.commit()”。这样即使插入的时候报错,修改的内容也不会提交到数据库,而如果没有手动进行setAutoCommit,那么出错的时候就会造成,前几条数据插入成功而后几条数据插入失败的情况,这就是脏数据。
- commit()
提交事务,用于把事务所做的修改都保存到数据库中,它把上一个commit或者rollback命令之后的全部事务都保存到数据库中。
- rollback()
撤销事务,在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已经完成的操作全部撤销,回滚到事务开始的状态,这里的操作特指数据库更新操作,并且回滚后,事务进入提交状态,因为回滚是回滚到事务开始时的状态。
#EL表达式的隐含对象
* applicationScope
应用程序范围内的scoped变量组成的集合
* cookie
所有cookie组成的集合
* header
HTTP请求头部,字符串
* headerValues
HTTP请求头部,字符串集合
* pageContext
当前页面的javax.servlet.jsp.PageContext对象
* initParam
全部应用程序参数名组成的集合
* pageScope
页面范围内所有对象的集合
* param
所有请求参数字符串组成的集合
* paramValues
所有作为字符串集合的请求参数
* requestScope
所有请求范围的对象的集合
* sessionScope
所有会话范围的对象集合
#简述Statement和PreparedStatement的区别
>无论多少次地使用同一SQL命令PreparedStatement都只对它解析和编译一次,当使用Statement对象时,每次执行一个SQL命令的时候都会对它解析和编译。因此使用PreparedStatement要比使用Statement速度要快。
#java中包的用途
* 允许将类文件组织起来,便于查找合适的类
* 包可以包含其他的,形成有层次的包空间
* 包有助于避免命名冲突
#请简述一下什么是流?
>流是指一连串流动的字符,以先进先出的方式发送和接收数据的通道。流分为输入流和输出流,相对于内存而言,数据输入到内存就是输入流,反之就是输出流。
#java.io.reader和java.io.inputStream的区别?
>两者共同组成了java输入类。Reader用于读入16位字符,即Unicode编码的字符;而InputStream用于读入ASCLL字符和二进制数据。
#请说出ArrayList和Vector的区别?
- 同步性
数组序列是线程不安全也不同步的,而矢量队列则完全相反。
- 数据增长
需要增长时,数组序列增长原来的一半,而矢量队列增长一倍。
#super关键字是干什么的?为什么使用它?它的访问范围?
>super用于解决如何直接访问或初始化从父类继承来的成员。
>在写子类无参数构造方法时,不用显式调用父类无参数构造方法,系统会自动提供,但在写子类带参数的构造方法时,应该在第一句写super(参数)来初始化父类成员变量
>访问范围包括父类属性、一般方法和构造方法
#Cookie和Session的区别与联系?
- Session的概念
Session 是存放在服务器端的,类似于Session结构来存放用户数据。当浏览器第一次发送请求时,服务器自动生成一个Session和Session ID用来标识这个Session,并将通过相应发送到浏览器。当浏览器第二次发送请求,会将前一次服务器响应中的Session ID放在请求中一并发送到服务器上,服务器从请求中提取Session ID并和保存的所有Session ID进行对比,找到这个用户对应的Session。一般浏览器提供了两种方式来保存Session,还有一种是程序员使用HTML隐藏域的方式自定义实现。
使用Cookie实现保存,这是最常见的方法,例如”记住我的登录状态”这一功能就是通过这种方式实现的。服务器通过设置Cookie的方式将Session ID发送到浏览器。如果我们不设置过期时间,那么Cookie将不会保存在硬盘上,会随着浏览器的关闭而消失,Session ID也就不复存在了。如果我们设置这个时间为若干天后,那么这个Cookie会保存在客户端硬盘中,即使浏览器关闭,这个值仍然存在,下次访问相应的网站时同样会发送到服务器上去。
使用URL附加信息的方式,这也是有时候我们会在JSP网站上看到”xxx.jsp?JSESSIONID=x”的原因。这种方式和使用Cookie的方式中不设置过期时间是一样的。
第三种方式是通过在页面表单里增加隐藏域的方式,这种方式实际上跟URL附加信息的方式一样,只不过前者使用GET方式发送数据,后者使用POST方式发送数据,但是明显后者比前者麻烦。
- 会话技术
会话指的是用户登录网站后的一系列操作,例如浏览商品->添加到购物车->提交订单->付款。会话跟踪时WEB程序中常用的技术,用户跟踪用户的整个会话,常用的会话跟踪技术就是Session和Cookie。Session在服务端记录信息并确认用户身份,Cookie在客户端记录信息并确认用户身份。
- 区别
Cookie存储在浏览器客户端,Session存储在服务端,简单说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上面,客户端每次请求服务器的时候会发送当前会话的session id,服务器根据当前session id判断相应用户数据标志,以确定用户是否登录或者具有某种权限。由于数据时存储在服务器上面,所以无法伪造,当时如果你能获取某个登录用户的session id,用特殊的浏览器伪造该用户的请求也是能够成功的。session id 是服务器和客户端链接时随机分配的,一般来说不会重复,但如果存在大量并发的请求,也不是没有重复的可能性。
- 联系
Cookie是Session的一种,但Cookie不会占用服务器资源,是存储在客户端内存或者一个cookie的文本文件中;而Session则会占用服务器资源,从这点上看,应该尽量使用Cookie而不是Session。
如果用户禁用cookie,那么可以采用url重写技术来进行页面处理,调用session中大量有用的方法从session中获取数据后置入页面。
- 应用场景
Cookie的典型应用时下次直接登录和在线商城的购物车的设计,当然这其中也会有一些安全和性能的问题存在。
由于Cookie的不安全性,所以一般会在Session上保存重要的信息。
#JSP的九大内置对象及其作用
>内置对象指的是可以不加声明和创建就可以在JSP脚本中使用的成员变量.
>产生的时机:一个JSP页面对应一个Servlet类,每个Servlet有三个方法。init()初始化JSP;destory()销毁JSP;service()对用户请求产生相应的方法。Request和response是service()的形参,application、page、out、pageContext、session这些对象都是在service()中生成的实例。
* javax.servlet.httpServletRequest
客户端的请求信息被封装在request对象中,主要用于接受通过HTTP协议传送到服务器的数据,包括头信息,系统信息,请求方式以及参数等,作用域为一次请求。
* javax.servlet.httpServletResponse
代表对客户端的相应,主要是将JSP容器处理过的对象传回客户端,response对象只在JSP页面有效。
* Session
Session是由服务器自动创建的与用户请求相关的对象,服务器为每个用户都生成一个session对象,用户保存用户的信息,跟踪用户的操作状态。它内部使用Map类来保存数据,即key/value,value可以是复杂的对象类型,而不仅仅局限于字符串类型。
* application
Application对象可以将信息保存到服务器直到服务器关闭为止,可以认为是系统中的全局变量,是ServletContext类的实例。
* out
此对象用于在web浏览器内输出信息,并且管理应用服务器上的输出缓冲区,是JspWriter的实例。
* pageContext
此对象提供了对JSP页面内所有对象以及命名空间的访问,它的本类名也叫做pageContext。
* config
获取服务器的配置信息
* page
它是java.lang.Object的实例,代表着JSP本身,本质上包含当前Servlet接口引用的变量,可以看做Java编程中的this
* exception
显示异常信息,只有在包含isErrorPage=”true”的页面上才可以使用。JAVA程序可以使用try/catch处理异常信息,但在JSP页面中出现异常就会生成exception对象,并把该对象传送到在page指令中设定的错误页面中,程序员在错误页面中处理exception对象即可。跟JAVA一样,JSP中exception对象也是由系统提供的继承机构,它实际上是java.lang.Throwable的对象。
#说一说你常用的Linux命令
* ls 显示文件或目录
-l 列出文件详细信息(list)
-a 列出当前目录下所有的文件及目录,包括隐藏文件(all)
* mkdir 创建目录
- p 创建目录,如果没有父目录则创建父目录(parent)
* cd 切换目录
* touch 创建空文件
* echo 创建带有内容的文件
* cat 查看文件内容
* cp 拷贝
* mv 移动或者重命名
* rm 删除文件
-r 递归删除,删除文件时一并删除子目录及文件
-f 强制删除
* find 在文件系统中搜索某文件
* wc 统计文本中行数、字数、字符数
* grep 在文本文件中查找某个字符串
* rmdir 删除空目录
* tree 显示目录为树形结构,需要安装tree包
* pwd 显示当前目录
* in 创建链接文件
* more、less 分页显示文本文件内容
* head、tail 显示文件头、尾内容
* Ctrl+alt+F1 命令行全屏模式
------系统管理命令------
* stat 显示指定文件的详细信息,比ls更详细
* who 显示在线登录用户
* whoami 显示当前操作用户
* hostname 显示主机名
* uname 显示系统信息
* top 动态显示当前耗费资源最多的进程信息
* ps 显示瞬时进程的运行状态,ps aux:以BSD格式显示进程;ps ef:以标准格式显示进程
* kill 杀死进程,一般使用ps或top命令查看进程的id,然后使用kill命令杀死对应进程。
* du 查看目录大小
-h 带有单位地显示目录信息
* df 查看磁盘大小
-h 带单位显示磁盘信息
* ifconfig 查看网络情况
* ping 测试网络连通
* netstat 显示网络状态信息
* man 帮助命令,使用-加上其他命令来查看对应参数的使用
* clear 清屏
* alias 对命令重命名
* sudo 允许系统管理员让普通用户执行一些或者root命令的工具
-b 在后台执行指令
-h 显示帮助
-H 将HOME环境变量设置为新身份的HOME环境变量
-k 结束密码的有效期,使用sudo的时候先输入密码,默认为5分钟有效期限
-l 列出目前用户可执行与无法执行的指令
-p 改变询问密码的提示符号
-s 执行指定的shell
-u<用户> 以指定的用户作为新的身份,若无此参数,则默认使用root为新的身份
-v 延长密码有效期限5分钟
-V 显示版本信息
------打包压缩相关命令------
* tar 打包压缩
-c 归档文件
-x 压缩文件
-v 显示压缩或解压缩过程(view)
-f 使用档名
-z gzip压缩文件
-j bzip2压缩文件
例如
tar -cvf /home/java.tar home/java 只打包不压缩
tar -zcvf /home/java.tar home/java 打包并使用gzip压缩
------打包压缩相关命令------
* shutdown
-r 关机重启
-h 关机不重启
now 立即关机
* halt 关机
* reboot 重启
------VIM相关命令------
Vim中有三种模式,命令模式、插入模式和编辑模式,使用ESC或i或:来切换模式。命令模式下
:q 退出
:q! 强制退出
:wq 保存并退出
:set number 显示行号
:set nonumber 隐藏行号
/java 在文档中查找java这个字符串,按n跳下一个,shift+n上一个
yyp 复制光标所在行,并粘贴
------文件权限管理------
R 读 数值表示为4
W 写 数值表示为2
X 可执行 数值表示为1
更改权限命令语法
sudo chmod[u=所属用户/g=所属组/o=其他用户/a所有用户][+=增加权限/-=减少权限][r/w/x] 目录/文件名
#Java运行时区域
程序计数器:记录当前指令所在单元地址,也就是程序执行.class文件某行。由于存储的只是一个行数number,所以区域较小。在多线程环境中,每个线程拥有一个程序计数器,分别记录着各自程序的执行程序所在行数。
JVM栈:存放执行的方法,方法中的局部变量和对象的引用(对象的实例存放在堆中)。由于拥有着先进后出的特性,所以可以看做一个桶。对桶中数据的操作成为压栈和弹栈。
本地方法栈:存放执行的本地方法,即使用native关键字修饰的方法。当程序执行到本地方法时,程序计数器不会记录,而是使用undefined替代(undefined和null的主要区别在于null表示此处不应该有值,而undefined表示此处有值但是还没有定义)
Java堆:存放类的实例和数组。分为新生代(young generation)和老年代(old generation),这样划分主要是为了方便垃圾回收器的工作,垃圾回收器会频繁地扫描新生代中的对象并进行垃圾回收,没有进行垃圾回收的对象就被移到了老年代中。
P:为了更加方便垃圾回收器的工作,JVM在新生代中又进行划分(eden和survicor)~~
方法区:又被称为非堆,存放类的基本信息以及常量(final)和静态(static)变量,这些数据被定义为永久代,垃圾回收器不会回收这些数据。
P1:此方法区跟java中的方法没有任何关系~
P2:JDK文档中说方法区是JAVA堆逻辑上的一部分。
P3:在JDK8中取消了永久代而是使用了metespace替代
运行时常量池:存放字面量(例如int number = 10 中的10就是一个字面量)和符号引用(例如类与引用名的全限定名,List deptList = new ArrayList();中的deptList实际是一个java.lang.List)
P1:常量池的出现是为了避免频繁的创建和销毁对象而影响到系统性能,其实现了对于对象的共享。例如字符串常量池就是在编译期间就把所有字符串文字放到一个常量池中。
P2:字符串常量池在JDK7时被移到了Java堆中(String name = “老王”中name作为引用存放到字符串常量池中,”老王”作为实例存放到堆中)。
P3:程序计数器、JVM栈和本地方法栈是线程私有的,Java堆、方法区和运行时常量池是线程共享的,因此一个多线程程序出现BUG,那么一定是线程共享中的区域出现了问题。
P4:Java规范中指出,除了程序计数器之外其他区域都有可能会发生OutOfMemoryError(内存溢出)异常。除了程序计数器和JVM栈其他区域都有可能会发生StackOverflowError(栈溢出)异常。使用JConsole(java自带)可以监控当前程序的内存和系统资源使用情况。
#常用的SQL语句
# stuscore 表中数据结构
stuid
name
subject
score
1
张三
数学
91
1
张三
英语
89
1
张三
语文
87
2
李四
语文
89
2
李四
英语
28
2
李四
数学
99
3
王五
语文
66
3
王五
数学
24
3
王五
英语
32
4
朱六
数学
88
>计算每个人的总成绩并排名,要求显示字段:名字和总成绩
思路:根据名字分组 group by 根据成绩排名 order by
SELECT name,SUM(score)
FROM stuscore
GROUP BY name
ORDER BY SUM(score)
>计算每个人的总成绩并排名,要求显示字段:名字、学号和总成绩
思路:DISTINCT 去重关键字 使用子查询先查询学号和总成绩
SELECT DISTINCT t1.name, subject,MAX(score)
FROM stuscore t1,
(SELECT stuid,SUM(score) AS allscore
FROM stuscore GROUP BY stuid) t2
WHERE t1.stuid=t2.stuid
ORDER BY t2.allscore DESC
>计算每个人单科的最高成绩,要求显示字段:学号、姓名、课程、最高成绩
思路:有两种写法
#写法一
SELECT stuid,name,subject,MAX(score)
FROM stuscore
GROUP BY stuid
#写法二
SELECT t1.stuid,t1.name,t1.subject,t1.score
FROM stuscore t1,
(SELECT stuid,MAX(score) AS maxscore
FROM stuscore GROUP BY stuid) t2
WHERE t1.stuid=t2.stuid AND t1.score=t2.maxscore
>计算每个人的平均成绩,要求显示字段:学号、成绩、平均成绩
思路:使用AVG()函数求出平均成绩,然后根据name分组
SELECT stuid,name,AVG(score)
FROM stuscore
GROUP BY name
>列出各门课程成绩最好的学生,要求显示字段:学号、姓名、科目、成绩
#写法一,这种写法会出现问题
SELECT stuid,name,subject,MAX(score)
FROM stuscore
GROUP BY subject
#写法二
SELECT s1.stuid,s1.name,s1.subject,s2.max_score
FROM stuscore s1,
(SELECT subject,MAX(score) AS max_score
FROM stuscore GROUP BY subject) s2
WHERE s1.subject=s2.subject AND s1.score=s2.max_score
>列出成绩最好的两位学生,要求显示字段:学号、姓名、成绩
思路:使用DESC将查询结果降序,然后使用limit截取列数
SELECT stuid,name,SUM(score) AS sumscore
FROM stuscore
GROUP BY name
ORDER BY sumscore DESC
LIMIT 2
#写法二,在SQL Server中的top关键字
SELECT top 2 * FROM stuscore ORDER BY sumscore DESC
>列出数学成绩最好的学生
SELECT name,subject,score
FROM stuscore
WHERE subject=’数学’
ORDER BY score DESC
>求出某位学生的数学成绩排名
思路:使用rownum临时变量输出mysql的排序后的行号
SELECT s.rowNo
FROM (SELECT (@rowNum:=@rowNum+1) AS rowNo,name,subject,score
FROM stuscore, (SELECT (@rowNum :=0)) b WHERE subject=’数学’
ORDER BY stuscore.’score’ DESC) s
WHERE name=’张三’
>统计学科成绩优、良、及格的个数
SELECT subject,
(SELECT COUNT(*) FROM stuscore WHERE score>80 AND score <=100) AS 优,
(SELECT COUNT(*) FROM stusocre WHERE score>60 AND score <80) AS 良,
(SELECT COUNT(*) FROM stuscore WHERE score<60) AS 不及格
FROM stuscore
GROUP BY subject
>使用SQL语句输出以下格式的数据
# 学号 姓名 语文 数学 英语 总分 平均分
SELECT stuid AS 学号,name AS 姓名,
SUM(CASE WHEN subject=’语文’ THEN score ELSE 0 END) AS 语文,
SUM(CASE WHEN subject=’数学’ THEN score ELSE 0 END) AS 数学,
SUM(CASE WHEN subject=’英语’ THEN score ELSE 0 END) AS 英语,
SUM(score) AS 总分,(SUM(score)/COUNT(*)) AS 平均分
FROM stuscore
GROUP BY stuid,name
>使用一条SQL语句查询出每门课都大于80的学生姓名
思路:使用HAVING()筛选成组后的各种数据,真实表中没有此数据,这些数据是通过一些函数生存。需要注意的是where子句在聚合前先筛选记录,作用于group by 和 having子句之前
SELECT name
FROM stusocre
GROUP BY name
HAVING MIN(score)>=80 AND COUNT(subject)>=3
>修改表结构,学历
#添加列信息
Alter table stuscore add 学历 varchar(6);
#删除列信息
Alter table stuscore drop column 学历;
>修改表数据 朱六
#将数学成绩改为90
UPDATE stuscore SET score=90 WHERE name LIKE ‘朱六’;
#删除表数据似乎不能使用like进行delete
DELETE FROM stuscore WHERE stuid=4;
>连接查询
# 当用户查看的数据来自于多张表的时候,连接查询会将多张表按照某个指定的条件进行记录的拼接。最终的结果是记录数可能会变化,但字段数一定会增加。
SELECT s1.*,s2.* FROM student AS s1 left join stuscore AS s2 ON s1.student_id = s2.stuscore_id
#能介绍一下redis吗?在项目中你是怎么使用它的?
>redis是最常用的非关系型数据库(Not-Only SQL)中的键值存储数据库(key-value),除了键值存储数据库之外还有文档型数据库(MongoDB)、图形(Graph)数据库(InfoGrid)、列存储数据库(HBase)。 对数据库高并发读写(High performance)的需求;对海量数据的高效率存储访问(Huge Storage)的需求;对数据库的高可扩展性(High Scalability)和高可用性( High Availability)的需求,带动了NOSQL的发展。
>redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,类似于memcached(比之出色的地方在redis的value的最大限制是1GB,而memcached只有1MB),它通过异步的方式将整个数据库加载到内存中然后进行操作,因为是纯内存操作,所以性能很好。不仅仅是出色的性能,它还通过提供多种键值数据类型来适应不同场景下的存储需求。
- 字符串类型
redis没有采用C语言对字符串的处理方式,而是自定义了一个数据结构SDS(imple dynamic string),简单动态字符串。打开redis源码包,在src下的sds.h文件下查看sds的源码如下:
struct sdshdr {
// 字符串长度
unsigned int len;
// buf数组中未使用的字节数量
unsigned int free;
// 用于保存字符串
char buf[];
};
>C语言对字符串的存储是使用字符数组,遇到'\0'字符则认为字符串结束,而redis的字符串可以存储任何类型的数据,因为任何类型数据都可以表示成二进制,sds结构中的char buf[]就是存储了二进制数据。并且redis的字符串是二进制安全的,什么是二进制安全?简单理解就是存入什么数据取出的还是什么数据。redis中的sds不像c语言处理字符串那样遇到'\0'字符则认证字符串结束,它不会对存储进去的二进制数据进行处理,存入什么数据取出还是什么数据。
>应用:自增主键,例如电商项目中的订单号和商品编号都是采用String的递增数字特性生成,使用INCR(此命令用于将key中存储的数字值增1)命令完成。
定义商品编号key:items:id
192.168.101.3:7003> INCR items:id
(integer) 2
192.168.101.3:7003> INCR items:id
(integer) 3
散列类型
如果有一个对象User以JSON序列化(String)的形式存储在redis,它的存储过程是User->JSON->Redis。该User对象有id、name、age等属性。如果在业务中只需要修改name属性,其他属性不做修改时该怎么做?如果采用传统的存储过程时必然会导致资源的的浪费,使用hash可以解决此问题。
hash会怎么做?它会提供字段与字段值的映射,并且规定字段值只能是字符串不能是散列或者集合类型。使用HSET(此命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0)命令对字段值做修改:
HSET user name laowang
应用:商品信息,包括商品id、商品名称、商品描述、商品库存、商品好评
商品信息在redis中为item:1001
获取命令
HGET items:1001 id
"3"
HGETALL items:1001
1) "id"
2) "3"
3) "name"
4) "apple"
5) "price"
6) "999.9"
列表类型
-arrayList && linkedList
ArrayList使用数组方式存储数据,所以根据索引查询数据速度快,而新增或者删除元素时需要设计到位移操作,所以比较慢。LinkedList使用双向链接(AB互相指向)方式存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元素的指针指向即可,速度非常快,然后通过下标查询元素时需要从头开始索引,所以比较慢,但是如果查询前几个元素或后几个元素速度比较快。
-List
列表类型可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为0(1)(算法中的时间复杂度,它和空间复杂度被合称为算法的复杂度,是衡量算法重要指标),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。
应用,商品评论列表
思路:在redis中创建商品评论列表,当用户发布商品评论时,将评论信息转成json存储到list中,用户在页面查询评论列表,从redis中取出json数据展示到页面。
定义商品评论列表key:
商品编号为1001的商品评论key:items: comment:1001
LPUSH items:comment:1001 '{"id":1,"name":"色情买家举报了!!!","date":20180510}'
集合类型
-set
集合的含义是每个元素的顺序不同,且没有顺序。集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的Redis内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为0(1)。 Redis还提供了多个集合之间的交集、并集、差集的运算。
有序集合类型
-
在集合类型的基础上有序集合类型为集合中的每个元素都关联一个分数,这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作。
在某些方面有序集合和列表类型有些相似。
1、二者都是有序的。
2、二者都可以获得某一范围的元素。
但是,二者有着很大区别:
1、列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。
2、有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快。
3、列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)
4、有序集合要比列表类型更耗内存。
根据商品销售量对商品进行排行显示,定义sorted set集合,商品销售量为元素的分数。
定义商品销售排行榜key:items:sellsort
写入商品销售量:
商品编号1001的销量是9,商品编号1002的销量是10
ZADD items:sellsort 9 1001 10 1002
商品编号1001的销量加1
ZINCRBY items:sellsort 1 1001
商品销量前10名:
ZRANGE items:sellsort 0 9 withscores
redis的主要缺点在于数据库容量受到物理内存的限制不能用作海量数据的高性能读写(意思就是要考虑那些低内存的用户电脑),因此redis最合适的使用场景是那些较小数据量的高性能操作和运算上,例如:
缓存(数据查询、短连接、新闻内容、商品内容等等)(最多使用)
分布式集群架构中的session分离。
聊天室的在线好友列表。
任务队列(秒杀、抢购、12306等等)
应用排行榜。
网站访问统计。
数据过期处理(可以精确到毫秒)
redis不仅仅只能用命令的形式操作,主流的后端语言都有对应的客户端支持,java官方推荐使用jedis和redisson。jedis托管在github上,你可以通过https://github.com/xetorthio/jedis访问和下载jedis客户端。
Redis的高性能是由于其将所有数据都存储在了内存中,为了使Redis在重启之后仍能保证数据不丢失,需要将数据从内存中同步到硬盘中,这一过程就是持久化。Redis支持两种方式的持久化,一种是RDB方式,一种是AOF方式。可以单独使用其中一种或将二者结合使用。
P:通过RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。如果数据很重要以至于无法承受任何损失,则可以考虑使用AOF方式进行持久化。
为了保证redis不会因为重启等原因而导致数据丢失,redis提供了持久化的解决方案,redis服务重新启动后会将硬盘中的数据恢复到内存中,但是此过程中如果redis服务器的硬盘损坏的话可能就会导致数据丢失。为了更好的保证数据的完整性,避免这种单点故障,可以使用redis提供了主从复制机制。主redis(master)中的数据有两个副本(replication)即从redis1(slave)和从redis2(slave),即使一台redis服务器宕机其它两台redis服务也可以继续提供服务
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略(回收策略),redis提供了6种数据淘汰策略。
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任何选择数据淘汰。
allkeys-lru:从数据集(server.db(i).dict)中挑选最近最少使用的数据淘汰。
allkeys-random:从数据集(server.db(i).dict)中任何选择数据淘汰。
no-enviction(驱逐):禁止驱逐数据。
Spring框架是一个为Java应用程序的开发提供了综合、广泛的基础性支持的java平台,也是一个实现IOC和AOP的容器框架,设计理念就是简化开发。
spring框架集成了许多模块,常用的有spring ioc、spring aop,spring mvc、spring boot、spring security。
使用spring的好处
在Hibernate中管理事务
public void save(){
Session session = sessionFactory.getCurrentSession();
Session.beginTransaction();
Info info = new Info(“百度”);
Info.setContent(“一家良心企业”);
session.save();
session.getTransaction().commit();
}
在JDBC中管理事务
Connection conn = null;
try{
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate(update person where name=”老王”)
conn.commit();
} catch(Exception e){
} final {conn.close()}
在spring中管理事务
@Transactional
public void save(){
sessionFactory.getCurrentSession.svae(info);
}
对象的创建都不再由程序员控制,而是交由Spring控制,称之为控制反转。
对象的依赖关系由Spring的配置文件描述,并且这种关系只有在使用的时候才会建立,称之为依赖注入。
spring中的org.springfremework.beans包和org.springfreamework.context包构成了spring框架的IoC容器的基础,org.springframework.beans.factory.BeanFactory是spring IoC容器的具体实现,用于包装和管理各种bean,因此该接口是spring IoC的核心接口。ApplicationContext接口对BeanFactory进行了扩展,提供了message resource用于国际化的机制、事务传播和应用层的特别配置,比如针对WEB应用的WebApplicationContext。
在Spring中的具体实现是将系统中的非核心业务基于代理机制做提取,进行单独处理。比如事务,比如日志,比如安全授权等。
Spring AOP就是采用动态代理实现的,spring中有两种动态代理方式,jdk代理和cglib代理。
Spring配置文件中的bean默认为单例
Spring提供了许多模板类用于解决代码的重复问题,例如RestTemplate、JmsTemplate、JpaTemplate
Spring 提供给了DispatcherServlet来对请求进行分发
Spring提供了一系列的JSP标签,高效宏来辅助视图
使用BeanFactory创建对象实例