大话-那些年常被问的java题

本文探讨了Java面试中常见的问题,包括`==`与`equals`的区别、重写`equals`与`hashCode`的必要性、String与StringBuffer/StringBuilder的性能对比,以及异常处理的`throw`与`throws`的用法。还涉及了哈希表的存储原理、线程安全的StringBuffer和非线程安全的StringBuilder,并简单提及了SpringBoot的拦截器和事务管理。
摘要由CSDN通过智能技术生成

背景:面试时,有些问题经常被问。现在来一探究竟。。。。
一、== 和 equals
默认情况,对于基本数据类型,==比较的是两个变量的值。对于引用对象,==比较的是两个对象的地址

Object 类
	public native int hashCode();
	
	public boolean equals(Object obj) {
        return (this == obj);
    }
	
 String 类 重写equals,hashcode
	public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
	
   //value -拆字符串,累加 hash
	public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

 HashMap 
		// key-value 取hash, Objects.hashCode(key)->object.hashCode();
		public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
		// 重写hashcode方法为了将数据存入HashSet/HashMap/Hashtable 类时进行比较
		public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }

分析可发现:
1、object 中,对于基本数据类型,==比较的是两个变量的值。对于引用对象,==比较的是两个对象的地址 ,equals比较的是对象地址
2、string重写后,string类型equals 比较的是值

二、一旦重写了equals方法,就一定要重写hashCode方法。为什么?
首先认识下啥是hashcode,

hashcode就是通过hash函数得来的,通俗的说,就是通过某一种算法得到的,hashcode就是在hash表中有对应的位置
对象的内部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode
HashCode的存在主要是为了查找的快捷性、hashcode代表对象就是在hash表中的位置

在这里插入图片描述
JDK API中关于Object类的equals和hashCode方法中扯了这么多
总结起来就是两句话:equals相等的两个对象的hashCode也一定相等,但hashCode相等的两个对象不一定equals相等。
为啥要重写,原因一、官方建议

接着我们通过代码证明,为啥要重写

public class Person {
    private String name;
    private int age;
    private String sex;

    Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    @Override
    public boolean equals(Object obj)
    {
        if(obj instanceof Person){
            Person person = (Person)obj;
            return name.equals(person.name);
        }
        return super.equals(obj);
    }
    @Override
    public int hashCode()
    {
        return name.hashCode();
    }

    public static void test(){
        HashMap<Person, Integer> map = new HashMap<Person, Integer>();

        Person p = new Person("jack",22,"男");
        Person p1 = new Person("jack",22,"男");

        System.out.println("p的hashCode:"+p.hashCode());
        System.out.println("p1的hashCode:"+p1.hashCode());
        System.out.println(p.equals(p1));
        System.out.println(p == p1);

        map.put(p,888);
        map.put(p1,888);
        map.forEach((key,val)->{
            System.out.println(key);
            System.out.println(val);
        });
    }

    public static void main(String[] args) {
        test();
    }
}
// 不重写hashcode运行结果:
p的hashCode:1205044462
p1的hashCode:761960786
true
false
com.gz.springboot_simple.Person@2d6a9952
888
com.gz.springboot_simple.Person@47d384ee
888
// 重写hashcode运行结果:
p的hashCode:3254239
p1的hashCode:3254239
true
false
com.gz.springboot_simple.Person@31a7df
888

分析发现:
1、不重写hashcode,map存2个对象、重写了存1个对象
2、我们重写equal 就是为了满足name相同,两个对象就是相同。很显然不重写hashcode出现了错误的预期
3、当我们用于存放在Hash相关的集合类中时,在重写equals时,需要重写hashCode,不然会出现与预期不符的结果
4、不重写取的都是object的hashcode,存在数组不同位置–会出现2个对象

为啥hash集合类就的重写hashcode,这得益于存储的数据结构—hash表
一个最简单的hash结构-
在这里插入图片描述
从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
在这里插入图片描述

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
	Node<K, V>[] tab;
	Node<K, V> p;
	int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length; // 当数组table为null时, 调用resize生成数组table, 并令tab指向数组table
	if ((p = tab[i = (n - 1) & hash]) == null) // 如果新存放的hash值没有冲突
		tab[i] = newNode(hash, key, value, null); // 则只需要生成新的Node节点并存放到table数组中即可
	else { // 否则就是产生了hash冲突
		Node<K, V> e;
		K k;
		if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
			e = p; // 如果hash值相等且key值相等, 则令e指向冲突的头节点
		else if (p instanceof TreeNode) // 如果头节点的key值与新插入的key值不等, 并且头结点是TreeNode类型,说明该hash值冲突是采用红黑树进行处理.
			e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 向红黑树中插入新的Node节点
		else { // 否则就是采用链表处理hash值冲突
			for (int binCount = 0;; ++binCount) { // 遍历冲突链表, binCount记录hash值冲突链表中节点个数
				if ((e = p.next) == null) { // 当遍历到冲突链表的尾部时
					p.next = newNode(hash, key, value, null); // 生成新节点添加到链表末尾
					if (binCount >= TREEIFY_THRESHOLD - 1) // 如果binCount即冲突节点的个数大于等于 (TREEIFY_THRESHOLD(=8) - 1),便将冲突链表改为红黑树结构, 对冲突进行管理,
															// 否则不需要改为红黑树结构
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 如果在冲突链表中找到相同key值的节点, 则直接用新的value覆盖原来的value值即可
					break;
				p = e;
			}
		}
		if (e != null) { // 说明原来已经存在相同key的键值对
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent为true表示仅当<key,value>不存在时进行插入, 为false表示强制覆盖;
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount; // 修改次数自增
	if (++size > threshold) // 当键值对数量size达到临界值threhold后, 需要进行扩容操作.
		resize();
	afterNodeInsertion(evict);
	return null;
}

分析可得:
1、hash表其实是一个数组,transient Node<K,V>[] table;
2、数据结构中有数组和链表来实现对数据的存储,但数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难。
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
3、哈希–综合两者的特性。如上图所示:
4、i = (n - 1) & hash 发现 hash & 位运算后,i 确定了数组下标

哈希表有多种不同的实现方法,最常用的一种方法—— 拉链法,我们可以理解为“链表的数组”
在这里插入图片描述

三、String,StringBuffer与StringBuilder
在这里插入图片描述
分析:
1、string—为final,是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象。所以尽量不在对String进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能。

// StringBuffer 
@Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

// StringBuilder	
	@Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
	
	public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        //增加容量-
        ensureCapacityInternal(count + len);
        //修改内容
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
	// char[] value;
	void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

分析
1、StringBuffer是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,2、StringBuffer是线程安全的可变字符串,其内部实现是可变数组。StringBuilder是jdk 1.5新增的,其功能和StringBuffer类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。

四、throw、Exception,throws
在这里插入图片描述
在这里插入图片描述
针对error-
1、StackOverflowError

 public static void main(String[] args) {
        test();
    }
    public static void test(){
        test();
    }
   // 运行结果:
   Exception in thread "main" java.lang.StackOverflowError
   分析:jvm 默认 -XX:ThreadStackSize=1024k 方法递归反复出入栈导致

2、OutOfMemoryError

设置jvm  -Xms10m -Xmx10m -XX:+PrintGCDetails
	public static void main(String[] args) {
        byte[] bytes=new byte[50*1024*1024];
    }
    //运行结果:
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    [GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1039K(9728K), 0.0401426 secs] [Times: user=0.00 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 2546K->498K(2560K)] 3087K->1556K(9728K), 0.0011030 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1582K->506K(2560K)] 2640K->1740K(9728K), 0.0009234 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 506K->474K(2560K)] 1740K->1708K(9728K), 0.0007213 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 474K->0K(2560K)] [ParOldGen: 1234K->1495K(7168K)] 1708K->1495K(9728K), [Metaspace: 3251K->3251K(1056768K)], 0.0113977 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 1495K->1495K(9728K), 0.0006521 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 1495K->1469K(7168K)] 1495K->1469K(9728K), [Metaspace: 3251K->3251K(1056768K)], 0.0106635 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

throw用于主动抛出java.lang.Throwable 类的一个实例化对象,意思是说你可以通过关键字 throw 抛出一个 Error 或者 一个Exception,如:throw new IllegalArgumentException(“size must be multiple of 2″),

而throws 的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。Java 中,任何未处理的受检查异常强制在 throws 子句中声明。

static void a() throws HighLevelException {
         try {
             b();
         } catch(MidLevelException e) {
             throw new HighLevelException(e);
         }
     }

五、拦截器 HandlerInterceptor, springboot集成-
HandlerExceptionResolver

@Slf4j
@Configuration
public class HandlerConfig extends WebMvcConfigurerAdapter  {
    @Resource
    private AuthSignInterceptor authSignInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("初始化拦截器 ok....");
        //自定义拦截器,添加拦截路径和排除拦截路径
        registry.addInterceptor(authSignInterceptor).addPathPatterns("/**").excludePathPatterns("api/login"); ;
    }
}

@Slf4j
@Component
public class AuthSignInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        log.info("进入 controller 之前,我来过。。。");
        log.info("请求地址:"+request.getRequestURI());
        // 1、重复提交 2、toekn过期校验 3、签名校验
        //preHandle() 方法:该方法会在控制器方法前执行,其返回值表示是否中断后操作。当其返回值为true时,表示继续向下执行;当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        log.info("走过了 controller view渲染之前,我来了");
        // postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        log.info("view渲染之后,我去了");
        // afterCompletion()方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。
    }
}

六、事务使用-transactionManager

<?xml version="1.0" encoding="UTF-8"?>
	<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	                    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
	                    http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

	<!-- Mybatis配置信息 分页拦截器 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	    <!--configLocation属性指定mybatis的核心配置文件-->
		<property name="dataSource" ref="mysqlDataSource" />
		<property name="configLocation" value="classpath:config/MyBatisConfiguration.xml" />
	</bean>
	
	<!-- 扫描basePackage下所有以@MyBatisRepository标识的 接口   创建MapperFactoryBean对象-->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com" />
		<property name="annotationClass" value="com.common.service.annotation.MyBatisRepository"/>
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	</bean>

	 <!-- 事务配置   spring管理mybatis的事务 -->
	 <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="mysqlDataSource"></property>
    </bean>

	<!-- 配置事务切面Bean,指定事务管理器 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<!-- 用于配置详细的事务语义 -->
		<tx:attributes>
			<!-- 所有以'save','del','update','add'开头的方法是write的 -->
			<tx:method name="save*" propagation="REQUIRED" rollback-for="Exception" />
			<tx:method name="del*" propagation="REQUIRED" rollback-for="Exception" />
			<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception" />
			<tx:method name="add*" propagation="REQUIRED" rollback-for="Exception" />
			<!-- 所有以'get','find','search','query'开头的方法是read-only的 -->
			<tx:method name="find*" propagation="SUPPORTS" />
			<tx:method name="get*" propagation="SUPPORTS" />
			<tx:method name="search*" propagation="SUPPORTS" />
			<tx:method name="query*" propagation="SUPPORTS" />
			<!-- 其他方法使用默认的事务设置 -->
			<tx:method name="*" />
			<!-- <tx:method name="*" propagation="REQUIRED" read-only="true"/> -->
		</tx:attributes>
	</tx:advice>

	<aop:config>
		<aop:pointcut id="serviceMethod"
			expression="(execution(* com.manage.web.service..*.*(..))) or (execution(* com.common.service..*.*(..)))" />
		<!-- 指定在serviceMethod切入点应用txAdvice事务切面 -->
		<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
	</aop:config>


	<!-- 强制使用CGLIB代理 默认false为JDK动态代理-->
	<aop:aspectj-autoproxy />
</beans>
	
	//自定义注解
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ElementType.TYPE})
	public @interface MyBatisRepository {}

分析:使用 @Service 、@MyBatisRepository 注解即可

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="${driver}" />
        <property name="url" value="${url}" />
        <property name="username" value="${username}" />
        <property name="password" value="${password}" />
        <!-- 初始化连接大小 -->  
        <property name="initialSize" value="${initialSize}"></property>
        <!-- 连接池最大数量 -->  
        <property name="maxActive" value="${maxActive}"></property>
        <!-- 连接池最大空闲 -->  
        <property name="maxIdle" value="${maxIdle}"></property>
        <!-- 连接池最小空闲 -->  
        <property name="minIdle" value="${minIdle}"></property>
        <!-- 获取连接最大等待时间 -->  
        <property name="maxWait" value="${maxWait}"></property>
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="numTestsPerEvictionRun" value="10" />
        <property name="minEvictableIdleTimeMillis" value="120000" />
    </bean>
  
    <!-- MyBatis -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:**/*Mapper.xml" />
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <value>
                            helperDialect=mysql
                        </value>
                    </property>
                </bean>
            </array>
        </property>
    </bean>
    

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.fsp.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

   

    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <tx:annotation-driven />
    
	
 mybatis插件配置:
	<?xml version="1.0" encoding="UTF-8"?>
	<!DOCTYPE configuration   
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
	<configuration>
		<settings>
			<!--<setting name="logImpl" value="LOG4J" />-->
			<setting name="callSettersOnNulls" value="true"/>
		</settings>
		<typeAliases>
			<typeAlias alias="sqlScript" type="com.fsp.vo.Sql"/>
		</typeAliases>
		<plugins>
			<plugin interceptor="cn.com.common.sdk.interceptor.SdkInterceptor"/>
		</plugins>
	</configuration>

分析: 使用:
@Service
@Transactional

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值