下面给出一个完整的 Java 实现:根据当前点的经纬度,对 API 返回的所有站点计算距离,筛选出 10 公里内的站点,并按距离由近到远排序。
一、定义站点模型
java
@Data public class Station { private Long id; private String name; private Double longitude; // 经度 private Double latitude; // 纬度 }返回结果可以复用站点模型,额外加一个距离字段:
java
@Data public class StationDistanceVO { private Long id; private String name; private Double longitude; private Double latitude; private Double distanceKm; // 距离,公里 }二、Haversine 距离工具类
java
public class GeoUtils { private static final double EARTH_RADIUS_KM = 6371.0; /** * 计算两个经纬度之间的距离,单位:公里 */ public static double distanceKm(double lat1, double lng1, double lat2, double lng2) { double dLat = Math.toRadians(lat2 - lat1); double dLng = Math.toRadians(lng2 - lng1); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS_KM * c; } }三、核心计算方法
java
public List<StationDistanceVO> findNearbyStations(double currentLat, double currentLng, List<Station> allStations, double radiusKm) { if (allStations == null || allStations.isEmpty()) { return Collections.emptyList(); } return allStations.stream() // 过滤掉经纬度为空的脏数据 .filter(s -> s.getLatitude() != null && s.getLongitude() != null) // 计算距离 .map(s -> { double distance = GeoUtils.distanceKm( currentLat, currentLng, s.getLatitude(), s.getLongitude() ); StationDistanceVO vo = new StationDistanceVO(); vo.setId(s.getId()); vo.setName(s.getName()); vo.setLongitude(s.getLongitude()); vo.setLatitude(s.getLatitude()); vo.setDistanceKm(distance); return vo; }) // 过滤出半径内的站点 .filter(vo -> vo.getDistanceKm() <= radiusKm) // 按距离升序排序 .sorted(Comparator.comparingDouble(StationDistanceVO::getDistanceKm)) .collect(Collectors.toList()); }四、调用示例
假设你从 API 拿到了站点列表:
java
// 当前点经纬度 double currentLat = 39.909; double currentLng = 116.397; // API 返回的所有站点 List<Station> allStations = stationApi.getAllStations(); // 计算 10 公里内的站点,按距离排序 List<StationDistanceVO> nearbyList = findNearbyStations( currentLat, currentLng, allStations, 10.0 ); // 输出 nearbyList.forEach(vo -> System.out.println(vo.getName() + " -> " + vo.getDistanceKm() + " km") );
五、如果需要分页
分页只需在排序后截取子列表:
java
int page = 1; int size = 20; int from = (page - 1) * size; int to = Math.min(from + size, nearbyList.size()); List<StationDistanceVO> pageList = nearbyList.subList(from, to); int total = nearbyList.size();
或者直接返回全部,由前端分页。数据量不大时推荐前端分页,避免多次计算。
六、性能优化建议
边界框预过滤:如果站点数量很大(上万),先用矩形范围缩小候选集,再精确计算 Haversine。
java
double latDelta = radiusKm / 111.0; double lngDelta = radiusKm / (111.0 * Math.cos(Math.toRadians(currentLat))); List<Station> candidates = allStations.stream() .filter(s -> Math.abs(s.getLatitude() - currentLat) <= latDelta && Math.abs(s.getLongitude() - currentLng) <= lngDelta) .collect(Collectors.toList());
然后再对 candidates 计算精确距离。
并行流:站点特别多时,可以用
parallelStream()加速距离计算,但要注意线程安全和结果顺序。排序后再collect,顺序仍然可控。缓存:如果当前点经常重复(比如热门商圈),可以把结果缓存到 Redis,设置短 TTL。
数据库层计算:如果站点在 MySQL 里,直接用 SQL 的 Haversine 公式 + 边界框 + 空间索引,减少数据传输量。
七、注意事项
经纬度顺序:Haversine 公式参数是
(lat1, lng1, lat2, lng2),别传反。空值处理:站点经纬度可能为 null,必须过滤,否则计算会 NPE。
距离单位:上面返回的是公里,如需米,乘以 1000。
浮点误差:
Math.sqrt(a)中 a 可能因浮点误差略大于 1,导致sqrt为 NaN。可以用Math.min(1, a)保护:java
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - Math.min(1, a)));
排序稳定性:距离相同的站点,可以再按 id 排序,保证结果可复现。
八、完整工具类
java
public class NearbyStationUtils { private static final double EARTH_RADIUS_KM = 6371.0; public static List<StationDistanceVO> findNearby(double lat, double lng, List<Station> stations, double radiusKm) { if (stations == null || stations.isEmpty()) { return Collections.emptyList(); } return stations.stream() .filter(s -> s.getLatitude() != null && s.getLongitude() != null) .map(s -> { double d = distanceKm(lat, lng, s.getLatitude(), s.getLongitude()); StationDistanceVO vo = new StationDistanceVO(); vo.setId(s.getId()); vo.setName(s.getName()); vo.setLongitude(s.getLongitude()); vo.setLatitude(s.getLatitude()); vo.setDistanceKm(d); return vo; }) .filter(vo -> vo.getDistanceKm() <= radiusKm) .sorted(Comparator.comparingDouble(StationDistanceVO::getDistanceKm)) .collect(Collectors.toList()); } public static double distanceKm(double lat1, double lng1, double lat2, double lng2) { double dLat = Math.toRadians(lat2 - lat1); double dLng = Math.toRadians(lng2 - lng1); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); a = Math.min(1, Math.max(-1, a)); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS_KM * c; } }九、总结
核心步骤:
拿到当前点经纬度和站点列表;
遍历站点,用 Haversine 公式计算距离;
过滤掉超过 10 公里的;
按距离升序排序;
返回结果,可分页。
如果站点很多,先用边界框缩小范围,再精确计算。注意空值、经纬度顺序和浮点误差保护。