code: @Override
    public <K, S, T> KVStreamlet<KeyedWindow<K>, T>
          join(Streamlet<S> otherStreamlet, SerializableFunction<R, K> thisKeyExtractor,
               SerializableFunction<S, K> otherKeyExtractor, WindowConfig windowCfg,
               SerializableBiFunction<R, S, ? extends T> joinFunction) {
      checkNotNull(otherStreamlet, "otherStreamlet cannot be null");
      checkNotNull(thisKeyExtractor, "thisKeyExtractor cannot be null");
      checkNotNull(otherKeyExtractor, "otherKeyExtractor cannot be null");
      checkNotNull(windowCfg, "windowCfg cannot be null");
      checkNotNull(joinFunction, "joinFunction cannot be null");
  
      return join(otherStreamlet, thisKeyExtractor, otherKeyExtractor,
          windowCfg, JoinType.INNER, joinFunction);
    }
docs: Return a new Streamlet by inner joining 'this streamlet with ‘other’ streamlet.
  The join is done over elements accumulated over a time window defined by windowCfg.
  The elements are compared using the thisKeyExtractor for this streamlet with the
  otherKeyExtractor for the other streamlet. On each matching pair, the joinFunction is applied.
  @param otherStreamlet The Streamlet that we are joining with.
  @param thisKeyExtractor The function applied to a tuple of this streamlet to get the key
  @param otherKeyExtractor The function applied to a tuple of the other streamlet to get the key
  @param windowCfg This is a specification of what kind of windowing strategy you like to
  have. Typical windowing strategies are sliding windows and tumbling windows
  @param joinFunction The join function that needs to be applied